feat(rennmausakte): Datenherkunft/Nachverfolgung pro Rennmaus
Neuer „Datenherkunft"-Button in der Rennmausakte öffnet einen Dialog, der zeigt, aus welchen Quellen der Eintrag erzeugt wurde: Quelldateien (Stammbäume + Wurfchronik), Anzahl zusammengeführter Datensätze, Eltern-Herleitung (Methode/Konfidenz) und Hinweise (z. B. „per manueller Entscheidung zugeordnet", „Konflikt gelöst", „aus N Datensätzen zusammengeführt"). Import: build_provenance() in merge_and_resolve.py sammelt die Herkunft über alle deduplizierten Datensätze und schreibt sie als Provenance-JSON je Tier in resolved_import.json. Backend: Gerbil.Provenance (nullable text) + Migration AddGerbilProvenance, gemappt im Ingest und im GerbilDto zurückgegeben. Frontend: ProvenanceDialog + Typen + Strings. Tests: Ingest-Round-trip (vorhanden/abwesend), e2e provenance.spec.ts. dotnet(212)/vitest(129)/playwright/tsc/eslint grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,15 @@ export function seedDb(): MockDb {
|
||||
enclosureId: 'enc-gross',
|
||||
originContactId: 'con-meier',
|
||||
originBreeder: 'Clan-Kleine-Chaoten',
|
||||
// NACHVERFOLGUNG: Datenherkunft (vom Import erzeugter JSON-String).
|
||||
provenance: JSON.stringify({
|
||||
sourceFiles: ['Stammbaum von Krümel.xlsx', 'Wurfchronik-Detail.docx'],
|
||||
mergedRecordCount: 2,
|
||||
fromWurfchronik: true,
|
||||
parentMethod: 'chart-position',
|
||||
parentConfidence: 'medium',
|
||||
notes: ['aus 2 Datensätzen zusammengeführt'],
|
||||
}),
|
||||
},
|
||||
{ ...gerbil('fridolin', 'Fridolin', 'male', '2023-05-01', 'w-fridolin', 'cv-schwarz', 'aa CC DD EE GG PP spsp rere'), enclosureId: 'enc-gross', originBreeder: 'Zoohandlung Meier', profilePhotoUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' },
|
||||
gerbil('luna', 'Luna', 'female', '2023-08-15', 'w-luna', 'cv-gold', 'AA CC DD EE GG pp spsp rere'),
|
||||
|
||||
46
gerbil-manager-web/e2e/provenance.spec.ts
Normal file
46
gerbil-manager-web/e2e/provenance.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/** NACHVERFOLGUNG: "Datenherkunft"-Button in der Tierakte öffnet den
|
||||
* Nachverfolgungs-Dialog mit Quelldateien, Datensatzanzahl und Hinweisen. */
|
||||
import { de, expect, skipUnlessMock, test } from './fixtures'
|
||||
|
||||
const p = de.provenance
|
||||
|
||||
test('Tierakte: "Datenherkunft"-Button öffnet den Nachverfolgungs-Dialog', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/rennmaeuse/kruemel')
|
||||
|
||||
// Button in den Aktionen öffnet den Dialog.
|
||||
await page.getByRole('button', { name: p.button }).click()
|
||||
const dialog = page.getByRole('dialog', { name: p.dialogTitle })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// Quelldateien werden angezeigt.
|
||||
await expect(dialog).toContainText(p.sourceFilesTitle)
|
||||
await expect(dialog).toContainText('Stammbaum von Krümel.xlsx')
|
||||
await expect(dialog).toContainText('Wurfchronik-Detail.docx')
|
||||
|
||||
// Zusammenführungs-Info + Wurfchronik-Hinweis.
|
||||
await expect(dialog).toContainText(p.mergedCount(2))
|
||||
await expect(dialog).toContainText(p.fromWurfchronik)
|
||||
|
||||
// Eltern-Herkunft (übersetzte Methode/Konfidenz).
|
||||
await expect(dialog).toContainText(p.methods['chart-position'])
|
||||
await expect(dialog).toContainText(p.confidences.medium)
|
||||
|
||||
// Hinweis aus dem Import.
|
||||
await expect(dialog).toContainText('aus 2 Datensätzen zusammengeführt')
|
||||
|
||||
// Schließen (Footer-Button — Header-Button trägt dasselbe aria-label).
|
||||
await dialog.locator('.provenance__actions').getByRole('button', { name: p.close }).click()
|
||||
await expect(dialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Tierakte: Tier ohne Importdaten zeigt "Keine Herkunftsdaten"', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
// Luna hat keine provenance im Mock → Leer-Zustand.
|
||||
await page.goto('/rennmaeuse/luna')
|
||||
|
||||
await page.getByRole('button', { name: p.button }).click()
|
||||
const dialog = page.getByRole('dialog', { name: p.dialogTitle })
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog).toContainText(p.empty)
|
||||
})
|
||||
Reference in New Issue
Block a user