Compare commits
5 Commits
feature/ge
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
| 08500757d7 | |||
| 36795bd974 | |||
| ae60b47ad3 | |||
| adb9e93cdb | |||
| 3aa9ac9811 |
@@ -291,7 +291,7 @@ public class ApplicationContext : DbContext
|
||||
(string Name, string Genotype, int SortOrder)[] catalog =
|
||||
{
|
||||
// --- C-locus white / partial albino (IDs 1-3) ---
|
||||
("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere", 0),
|
||||
("REW", "AA chch DD EE GG pp spsp rere", 0),
|
||||
("Hermelin", "aa chch DD EE GG PP spsp rere", 1),
|
||||
("Himalaya", "AA chch DD EE GG PP spsp rere", 2),
|
||||
// --- Zobel / colourpoint dark (ID 4) ---
|
||||
|
||||
1430
GerbilManagerWebAPI/Migrations/20260606231220_RenameREW.Designer.cs
generated
Normal file
1430
GerbilManagerWebAPI/Migrations/20260606231220_RenameREW.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
GerbilManagerWebAPI/Migrations/20260606231220_RenameREW.cs
Normal file
33
GerbilManagerWebAPI/Migrations/20260606231220_RenameREW.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerbilManagerWebAPI.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameREW : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
|
||||
column: "Name",
|
||||
value: "REW");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "ColorVarieties",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
|
||||
column: "Name",
|
||||
value: "Pink Eyed White (PEW)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ namespace GerbilManagerWebAPI.Migrations
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000001"),
|
||||
CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
|
||||
Name = "Pink Eyed White (PEW)",
|
||||
Name = "REW",
|
||||
SortOrder = 0
|
||||
},
|
||||
new
|
||||
|
||||
@@ -50,28 +50,30 @@ Weitere Eigenschaften lassen sich jederzeit **additiv** ergänzen (1-Zeilen-Änd
|
||||
|
||||
Durch die vielen neuen Stammbaum-Dateien sind **13 neue Konflikt-Tiere** aufgetaucht (**7 erledigt:** Kazumi/Filou/Sokrates/Osamu/Percy/Iwana/Eragon ✅ — **6 offen**) (gleicher Name+Datum, widersprüchliche Angaben in mehreren Diagrammen). Sie warten in Quarantäne — **nichts ist verloren**, sie laden automatisch nach, sobald du je Tier kurz sagst was stimmt. (Uw=G + „Vorhandensein gewinnt" sind schon angewendet; das hier ist der echte Rest.)
|
||||
|
||||
_Quelldatei = die Stammbaum-Datei(en) in `C:\Users\gulum\dev\Sttammbäume\`, in denen das Tier vorkommt. **Mehrere Dateien = Ursache des Konflikts** (widersprüchliche Angaben in verschiedenen Diagrammen)._
|
||||
|
||||
**A) Nur Sterbedatum offen** (Gencode einig — bei Osamu/Filou/Sunny zusätzlich „taub" beibehalten):
|
||||
| Tier | Sterbedatum — welches? |
|
||||
|---|---|
|
||||
| Isa of Golden Lights (*24.12.2014) | 21.07.2018 ↔ 21.10.2018 |
|
||||
| Jack II v.d. K.C. (*14.02.2016) | 06.10.2019 ↔ 20.10.2019 |
|
||||
| ~~Osamu v.d. K.C. (*10.12.2015)~~ ✅ | **18.12.2020** (erledigt) |
|
||||
| ~~Filou v.d. K.C. (*24.11.2014)~~ ✅ | **31.08.2019** (erledigt) |
|
||||
| Sunny von PZ Karl (*10.04.2014) | 30.04.2019 ↔ 05.05.2019 |
|
||||
| Tier | Sterbedatum — welches? | Quelldatei(en) |
|
||||
|---|---|---|
|
||||
| Isa of Golden Lights (*24.12.2014) | 21.07.2018 ↔ 21.10.2018 | Stammbaum von **Ella**.xlsx · von **Kalea**.xlsx |
|
||||
| Jack II v.d. K.C. (*14.02.2016) | 06.10.2019 ↔ 20.10.2019 | Stammbaum von **Kalea**.xlsx · von **Rainny**.xlsx · von **Ren**.xlsx |
|
||||
| ~~Osamu v.d. K.C. (*10.12.2015)~~ ✅ | **18.12.2020** (erledigt) | — |
|
||||
| ~~Filou v.d. K.C. (*24.11.2014)~~ ✅ | **31.08.2019** (erledigt) | — |
|
||||
| Sunny von PZ Karl (*10.04.2014) | 30.04.2019 ↔ 05.05.2019 | Stammbaum von **Vance**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
|
||||
|
||||
**B) Gencode-Konflikt** (+ ggf. Sterbedatum):
|
||||
| Tier | Konflikt — was stimmt? |
|
||||
|---|---|
|
||||
| Milon v.d. K.C. (*27.11.2014) | A-Locus: **Aa** ↔ **aa** |
|
||||
| ~~Percy of little runners (*16.12.2017)~~ ✅ | P-Locus **Pp** (erledigt) |
|
||||
| ~~Iwana of little runners (*02.10.2018)~~ ✅ | P-Locus **Pp** (erledigt) |
|
||||
| ~~Sokrates v.d. K.C. (*14.12.2015)~~ ✅ | **D-** + Sterbedatum **20.05.2019** (erledigt) |
|
||||
| ~~Eragon (Elieus, *18.05.2016)~~ ✅ | C **CC** (vollfarbig) + Name **„Elieus gen. Eragon"**. Korrekter Datensatz lädt bereits (CC); Colourpoint-Variante „Kleiner Warnowrenner …" bleibt als Dublette in Quarantäne — kein weiterer Schritt nötig. |
|
||||
| Dakota of sweet little mouse (*30.01.2015) | A: **Aa**↔**aa** · P: **pp**↔**PP** · Sp: **Spsp**↔**spsp** |
|
||||
| ~~Kazumi v.d. K.C. (*23.04.2013)~~ ✅ | **Aa Cc[chm] DD ee[f] GG PP Spsp** (erledigt) |
|
||||
| Max von Privat (*01.02.2013) | D: **D-**↔**DD** · P: **P-**↔**PP** · Sterbedatum (4 Varianten: 04.02.2016 / 04.03.2016 / 2014 / 30.12.2015) |
|
||||
| Tier | Konflikt — was stimmt? | Quelldatei(en) |
|
||||
|---|---|---|
|
||||
| Milon v.d. K.C. (*27.11.2014) | A-Locus: **Aa** ↔ **aa** | Stammbaum von **South Dakota**.xlsx · von **Tennessee**.xlsx |
|
||||
| ~~Percy of little runners (*16.12.2017)~~ ✅ | P-Locus **Pp** (erledigt) | — |
|
||||
| ~~Iwana of little runners (*02.10.2018)~~ ✅ | P-Locus **Pp** (erledigt) | — |
|
||||
| ~~Sokrates v.d. K.C. (*14.12.2015)~~ ✅ | **D-** + Sterbedatum **20.05.2019** (erledigt) | — |
|
||||
| ~~Eragon (Elieus, *18.05.2016)~~ ✅ | C **CC** (vollfarbig) + Name **„Elieus gen. Eragon"**. Korrekter Datensatz lädt bereits (CC); Colourpoint-Variante „Kleiner Warnowrenner …" bleibt als Dublette in Quarantäne — kein weiterer Schritt nötig. | — |
|
||||
| Dakota of sweet little mouse (*30.01.2015) | A: **Aa**↔**aa** · P: **pp**↔**PP** · Sp: **Spsp**↔**spsp** | Stammbaum von **Jiminy of Black Forest**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
|
||||
| ~~Kazumi v.d. K.C. (*23.04.2013)~~ ✅ | **Aa Cc[chm] DD ee[f] GG PP Spsp** (erledigt) | — |
|
||||
| Max von Privat (*01.02.2013) | D: **D-**↔**DD** · P: **P-**↔**PP** · Sterbedatum (4 Varianten: 04.02.2016 / 04.03.2016 / 2014 / 30.12.2015) | Stammbaum von **Danako**.xlsx · von **Kalea**.xlsx · von **Vance**.xlsx · von **Wildfire und Vestras Kids**.xlsx · von **Yurikas und Pintos Sohn**.xlsx |
|
||||
|
||||
*(Alle Gencode-Varianten + Quelldateien: `tools/import/output/review-report.md`.)*
|
||||
*(Alle Gencode-Varianten + Quelldateien-Details: `tools/import/output/review-report.md`.)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -110,3 +110,30 @@ test('+-Knopf ist sichtbar und lädt weitere Vorfahren nach (STAMMBAUM-EXPAND)
|
||||
await expandBtn.click({ force: true })
|
||||
await expect(page.getByRole('link', { name: 'Max' })).toBeVisible({ timeout: 8000 })
|
||||
})
|
||||
|
||||
test('Würfe-Panel zeigt Würfe des Wurzeltiers + Link öffnet Wurf (STAMMBAUM-LITTERS)', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
// Fridolin ist Vater von Wurf K (5 Junge) — Panel muss erscheinen.
|
||||
await page.goto('/rennmaeuse/fridolin/stammbaum')
|
||||
await expect(page.locator('.pedigree-card').first()).toBeVisible()
|
||||
|
||||
const panel = page.locator('.stammbaum-litters-panel')
|
||||
await expect(panel).toBeVisible()
|
||||
await expect(panel).toContainText(t.littersTitle)
|
||||
await expect(panel).toContainText('Wurf K')
|
||||
await expect(panel).toContainText('5')
|
||||
|
||||
// Link-Klick → Wurf-Detailseite
|
||||
const wurfLink = panel.getByRole('link', { name: /Wurf K/ })
|
||||
await expect(wurfLink).toBeVisible()
|
||||
await wurfLink.click()
|
||||
await expect(page).toHaveURL(/\/wuerfe\/w-kruemel/)
|
||||
})
|
||||
|
||||
test('Kein Würfe-Panel wenn Wurzeltier keine Würfe hat (STAMMBAUM-LITTERS)', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
// Krümel hat noch keine Würfe als Elternteil → Panel muss fehlen.
|
||||
await page.goto('/rennmaeuse/kruemel/stammbaum')
|
||||
await expect(page.locator('.pedigree-card').first()).toBeVisible()
|
||||
await expect(page.locator('.stammbaum-litters-panel')).not.toBeVisible()
|
||||
})
|
||||
|
||||
@@ -22,9 +22,10 @@ import Tree from 'react-d3-tree'
|
||||
import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree'
|
||||
import { de } from '../strings/de'
|
||||
import { ApiError } from '../api/client'
|
||||
import { listLitters } from '../api/litters'
|
||||
import { listColorVarieties } from '../api/lookups'
|
||||
import { getInbreedingCoefficient } from '../api/pedigree'
|
||||
import type { Gender, Gerbil } from '../api/types'
|
||||
import type { Gender, Gerbil, Litter } from '../api/types'
|
||||
import { useApi } from '../hooks/useApi'
|
||||
import { formatDate, genderLabel } from '../format/labels'
|
||||
import { UNKNOWN_FARBSCHLAG, fromDisplayString, genotypeToFarbschlag } from '../genetics'
|
||||
@@ -156,6 +157,13 @@ export default function StammbaumPage() {
|
||||
? `${(inbreeding.data * 100).toLocaleString('de-DE', { maximumFractionDigits: 1 })} %`
|
||||
: t.inbreeding.unavailable
|
||||
|
||||
/* ── Würfe des Wurzeltiers (STAMMBAUM-LITTERS): aktualisiert bei Umwurzeln ── */
|
||||
const rootLitters = useApi(
|
||||
() => listLitters({ filter: `fatherId=${id}|motherId=${id}`, orderBy: 'date desc', pageSize: 50 }),
|
||||
[id],
|
||||
)
|
||||
const rootLitterItems = rootLitters.data?.items ?? []
|
||||
|
||||
/* ── react-d3-tree-Daten ── */
|
||||
const nodesByPath = useMemo(() => (root ? collectNodes(root) : null), [root])
|
||||
const datum = useMemo(() => (root ? toRawNodeDatum(root, t.unknown) : null), [root, t])
|
||||
@@ -315,6 +323,10 @@ export default function StammbaumPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="stammbaum-layout">
|
||||
{rootLitterItems.length > 0 && (
|
||||
<LittersPanel litters={rootLitterItems} t={t} />
|
||||
)}
|
||||
<div className="stammbaum-canvas" ref={canvasRef}>
|
||||
{view && (
|
||||
<Tree
|
||||
@@ -335,6 +347,7 @@ export default function StammbaumPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="stammbaum-hints">
|
||||
<li>{t.tapHint}</li>
|
||||
<li>{t.hintName}</li>
|
||||
@@ -417,6 +430,36 @@ function PedigreeCard({
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Würfe-Panel (STAMMBAUM-LITTERS) ─────────────────────────────── */
|
||||
|
||||
function LittersPanel({
|
||||
litters,
|
||||
t,
|
||||
}: {
|
||||
litters: Litter[]
|
||||
t: { littersTitle: string; littersJunge: string }
|
||||
}) {
|
||||
return (
|
||||
<aside className="stammbaum-litters-panel" aria-label={t.littersTitle}>
|
||||
<div className="stammbaum-litters-panel__title">{t.littersTitle}</div>
|
||||
<ul className="stammbaum-litters-panel__list">
|
||||
{litters.map((l) => (
|
||||
<li key={l.id}>
|
||||
<Link to={`/wuerfe/${l.id}`} className="stammbaum-litters-panel__link">
|
||||
<span className="stammbaum-litters-panel__name">{l.name}</span>
|
||||
{l.totalBorn != null && (
|
||||
<span className="stammbaum-litters-panel__born">
|
||||
{l.totalBorn} {t.littersJunge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function SexIcon({ gender }: { gender: Gender }) {
|
||||
const symbol = gender === 'male' ? '♂' : gender === 'female' ? '♀' : '?'
|
||||
return (
|
||||
|
||||
@@ -45,9 +45,132 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* ── Würfe-Panel + Layout (STAMMBAUM-LITTERS) ────────────────── */
|
||||
|
||||
.stammbaum-layout {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Desktop: Würfe-Panel links vom Baum. */
|
||||
.stammbaum-litters-panel {
|
||||
flex: none;
|
||||
width: 148px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.5rem 0.75rem 0.5rem 0;
|
||||
border-right: 1px solid var(--color-border);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
overflow-y: auto;
|
||||
max-height: calc(clamp(18rem, 62dvh, 46rem) - 3rem);
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
padding: 0.3rem 0.45rem;
|
||||
border-radius: 0.4rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 0.8rem;
|
||||
font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__link:hover {
|
||||
background: var(--color-accent-soft);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__name {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__born {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
/* Mobil (≤520px): Panel als kompakter horizontaler Streifen ÜBER dem Baum. */
|
||||
@media (max-width: 520px) {
|
||||
.stammbaum-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel {
|
||||
width: auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__title {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__list {
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
max-height: none;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__link {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stammbaum-litters-panel__born {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Canvas behält explizite Höhe in column-Richtung. */
|
||||
.stammbaum-canvas {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Zeichenfläche ────────────────────────────────────────────── */
|
||||
|
||||
.stammbaum-canvas {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: clamp(18rem, 62dvh, 46rem);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.6rem;
|
||||
|
||||
@@ -358,6 +358,9 @@ export const de = {
|
||||
/** Mini-Legende unter dem Baum (STAMMBAUM-EXPAND). */
|
||||
hintName: 'Namenslink: Tierakte öffnen',
|
||||
hintExpand: '+: weitere Vorfahren nachladen',
|
||||
/** Würfe-Panel links (STAMMBAUM-LITTERS). */
|
||||
littersTitle: 'Würfe',
|
||||
littersJunge: 'Junge',
|
||||
zoomIn: 'Vergrößern',
|
||||
zoomOut: 'Verkleinern',
|
||||
zoomFit: 'Ansicht einpassen',
|
||||
|
||||
Reference in New Issue
Block a user