Files
Gulum 45c3f87f01
All checks were successful
CI / Backend Tests (.NET) (push) Successful in 1m16s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m38s
CI / Docker Build & Push (push) Successful in 1m32s
CI / Deploy auf TrueNAS (Custom App) (push) Successful in 9s
fix(akte): falscher Hinweis „Weicht vom eingetragenen Farbschlag ab"
Beim Prod-Nachtest aufgefallen: Bei Schecken meldete die Akte eine Abweichung,
obwohl eingetragener und errechneter Farbschlag identisch angezeigt wurden
(JackJack: „Saphir Schecke" vs. „Saphir Schecke"). Zwei Ursachen:

- Der Vergleich schnitt „Schecke"/„Rex" nur auf der ERRECHNETEN Seite ab, nicht
  auf der eingetragenen (die den Zusatz über formatVarietyName trägt). Jetzt
  werden beide Seiten gleich normalisiert.
- formatVarietyName hängte den Modifier auch an, wenn der Katalogname ihn schon
  trägt („Schwarz Schecke" → „Schwarz Schecke Schecke").

Tests: 3 vitest-Fälle zu formatVarietyName, e2e SCHECKE-MISMATCH mit Gegenprobe
(Mock-Tier mit echter Abweichung zeigt den Hinweis weiter).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:22:59 +02:00

671 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* QA-1: Seed-Daten für den Mock-Modus — geformt exakt nach dem DATA-2-Vertrag
* (camelCase-DTOs, Gridify-paged Listen). Datensatz angelehnt an Kellys
* FEAT-4-Verifikationsdaten (mehrgenerationiger Stammbaum inkl. gemeinsamem
* Vorfahren Anton), erweitert um Becken/Kontakte/Gesundheit/Gewicht für die
* FEAT-2/FEAT-6-Flüsse.
*/
import type { Contact, Enclosure, Gerbil, Litter } from '../src/api/types'
import type { InboxRequest } from '../src/api/requests'
export interface HealthRecordRow {
id: string
gerbilId: string
date: string
type: string
description: string
veterinarian: string | null
createdAt: string
}
export interface WeightRecordRow {
id: string
gerbilId: string
date: string
weightGrams: number
notes: string | null
}
/** WEB-0b: CMS-Block (data ist das typ-spezifische JSON-Objekt). */
export interface MockBlock {
id: string
order: number
type: string
data: Record<string, unknown>
}
/** WEB-0b: CMS-Seite der öffentlichen Webseite. */
export interface MockPage {
id: string
slug: string
title: string
seoDescription: string | null
status: 'Draft' | 'Published'
blocks: MockBlock[]
}
/** ABGABE: Abgabevertrag (mirrors SaleContract in src/api/contracts.ts). */
export interface MockContract {
id: string
contactId: string
price: number
handoverDate: string
contractDate: string
fileName: string
createdAt: string
gerbilIds: string[]
url: string
/** Ob die Word-Datei vorliegt. Importierte Verträge: false (kein Word-Button). */
hasFile: boolean
}
export interface MockDb {
gerbils: Gerbil[]
litters: Litter[]
enclosures: Enclosure[]
contacts: Contact[]
colorVarieties: { id: string; name: string; canonicalGenotype: string | null; sortOrder: number }[]
healthRecords: HealthRecordRow[]
weightRecords: WeightRecordRow[]
pages: MockPage[]
// INBOX-1: Anfragen-Posteingang. Die Flags steuern die Fehlerpfade des Mocks
// (Specs können sie pro Test umlegen): KI-Entwurf 503 / Sync+Send Mail-Fehler.
requests: InboxRequest[]
aiConfigured: boolean
mailConfigured: boolean
// ABGABE: Verträge + KI-Inserat-Flag
contracts: MockContract[]
saleAdConfigured: boolean
// FEAT-NAMEGEN: Namensvorschläge — false = 503 NamesKeyMissing simulieren
namesConfigured: boolean
// FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback)
feedback: Record<string, unknown>[]
// GEPRÜFTE TIERE: „vollständig korrekt"-Markierungen / Schutz (/verified-gerbils)
verified: Record<string, unknown>[]
// RPRO3: RennmausPro-III-Import — feste Auswertung + Import-Ergebnis für die UI-Specs.
rpro3: {
analyze: Record<string, unknown>
execute: Record<string, unknown>
}
// ERWERB: Erwerb/Kauf je Tier (Kaufdatum, Preis, Notiz) — /acquisitions
acquisitions: Record<string, unknown>[]
// ABGABE-STATUS: Reservierungs-/Abgabe-Status (POST/PUT/DELETE /reservations)
reservations: Record<string, unknown>[]
// WAITLIST: Warteliste/Nachfrage (RennmausPro nachfrage_tb)
waitingList: Record<string, unknown>[]
// RÜCKNAHMEN: zurückgenommene Tiere (CRUD /returns)
returns: Record<string, unknown>[]
// EXHIBITION: Ausstellungs-/Show-Ergebnisse je Tier (POST /exhibitions)
exhibitions: Record<string, unknown>[]
}
function gerbil(
id: string,
name: string,
gender: 'male' | 'female',
dateOfBirth: string,
litterId: string | null,
colorVarietyId: string | null,
genotype: string | null = null,
): Gerbil {
return {
id,
name,
gender,
status: 'Breeding',
dateOfBirth,
dateOfDeath: null,
causeOfDeath: null,
goHomeDate: null,
litterId,
enclosureId: null,
colorVarietyId,
originContactId: null,
receiverContactId: null,
originBreeder: null,
// SEARCH-2b: backend stores a normalized name (lowercase, [\s._-] stripped).
nameSearch: name.toLowerCase().replace(/[\s._-]/g, ''),
genotype,
notes: null,
// BESTAND-FILTER: default = eigener Bestand; einzelne externe Ahnen unten gesetzt.
isResident: true,
profilePhotoUrl: null,
}
}
/** Frischer, unabhängiger Datenbestand pro Test. */
export function seedDb(): MockDb {
const gerbils: Gerbil[] = [
{
...gerbil('kruemel', 'Krümel', 'female', '2025-03-12', 'w-kruemel', 'cv-agouti', 'Aa CC DD EE GG Pp spsp rere'),
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'],
history: [
'In „Stammbaum von Krümel.xlsx“ gefunden.',
'Geburtsdatum (12.03.2025) aus „Stammbaum von Krümel.xlsx“.',
'Genotyp aus „Stammbaum von Krümel.xlsx“.',
'Auch in „Wurfchronik-Detail.docx“ gefunden → Datensätze zusammengeführt.',
// VERWORFEN: abweichender Wert aus einer Quelle wurde verworfen (Grund + Ersatz).
'⚠ Geburtsdatum 14.06.2015 aus „Wurfchronik-Detail.docx“ verworfen — abweichend; 12.03.2025 aus „Stammbaum von Krümel.xlsx“ verwendet (Mehrheit).',
'Eltern über Position im Stammbaum erkannt (Quelle: „Stammbaum von Krümel.xlsx“).',
// VERWORFEN: unplausibler Elternteil verworfen, kein Ersatz verwendet.
'⚠ Vater „Jayjay“ (*19.06.2013) verworfen — unplausibel (9 Jahre älter als das Kind); kein Ersatz.',
'Angaben aus der Wurfchronik übernommen.',
],
}),
},
{ ...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'),
gerbil('balu', 'Balu', 'male', '2021-04-20', 'w-balu', 'cv-agouti'),
gerbil('maja', 'Maja', 'female', '2021-06-11', null, 'cv-schwarz-schecke', 'aa CC DD EE GG PP Spsp rere'),
gerbil('karlsson', 'Karlsson', 'male', '2022-02-02', 'w-karlsson', 'cv-blau'),
// FARBSCHLAG-MISMATCH: eingetragen „Schwarz Schecke", Gencode ergibt Agouti Schecke
// → der Hinweis „Weicht vom eingetragenen Farbschlag ab" MUSS hier erscheinen.
gerbil('mismatch-maus', 'Mismatch Maus', 'female', '2023-02-02', null, 'cv-schwarz-schecke',
'AA CC DD EE GG PP Spsp rere'),
gerbil('smilla', 'Smilla', 'female', '2022-09-30', null, 'cv-himalaya'),
gerbil('anton', 'Anton', 'male', '2019-07-07', 'w-anton', 'cv-agouti'),
gerbil('greta', 'Greta', 'female', '2019-05-23', null, 'cv-schwarz'),
gerbil('frieda', 'Frieda', 'female', '2020-01-18', null, 'cv-gold'),
gerbil('emil', 'Emil', 'male', '2017-03-03', 'w-emil', 'cv-agouti'),
// BESTAND-FILTER: externe Ahnen (Gründertiere fremder Zuchten, nur für den Stammbaum).
{ ...gerbil('hilde', 'Hilde', 'female', '2017-11-11', null, 'cv-schwarz'), isResident: false, originBreeder: 'Zoohandlung Meier' },
{ ...gerbil('max', 'Max', 'male', '2015-08-08', null, 'cv-agouti'), isResident: false, originBreeder: 'Zoohandlung Meier' },
// Statistik: Verstorbene + Abgegebene für Verluste/Bestandskurve
{ ...gerbil('willi', 'Willi', 'male', '2019-09-09', null, 'cv-schwarz'), status: 'Deceased', dateOfDeath: '2022-04-04' },
{ ...gerbil('pauli', 'Pauli', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-huber' },
// Ticket 24522f5f (JackJack): abgegeben UND später beim Abnehmer verstorben —
// Status Deceased, aber Abnehmer + Abgabedatum gesetzt. Die Akte muss beides zeigen.
{
...gerbil('rosa', 'Rosa', 'female', '2020-02-02', null, 'cv-gold'),
status: 'Deceased',
dateOfDeath: '2023-08-15',
receiverContactId: 'con-huber',
goHomeDate: '2022-01-15',
},
{ ...gerbil('pauli-breeder', 'Pauli Breeder', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-meier' },
{ ...gerbil('pauli-neither', 'Pauli Neither', 'male', '2023-05-01', null, 'cv-himalaya'), status: 'GivenAway', goHomeDate: '2023-07-15', receiverContactId: 'con-neither' },
// ABGABE: zwei Tiere zur Abgabe im Quarantänebecken (enc-leer ist leer, kein Konflikt
// mit dem Becken-Belegungstest der Großbecken-2-Tiere-Assertion).
{
...gerbil('sale-balu', 'Balu Abgabe', 'male', '2024-03-12', null, 'cv-agouti'),
status: 'ForSale' as const,
enclosureId: 'enc-leer',
notes: 'ruhig, nimmt Leckerlis aus der Hand',
},
{
...gerbil('sale-benny', 'Benny Abgabe', 'male', '2024-03-12', null, 'cv-schwarz-schecke'),
status: 'ForSale' as const,
enclosureId: 'enc-leer',
notes: 'neugieriger Entdecker',
},
// LITTER-JUVENILE-DETAILS: Jungtiere für Wurf K mit Zusatzinfos.
// JUVENILE-AVATARS: Pippa hat profilePhotoUrl (1×1-PNG data-URI) → Avatar-Img sichtbar.
{
...gerbil('pup-abgabe', 'Pippa', 'female', '2025-03-12', 'w-kruemel', 'cv-gold'),
status: 'GivenAway' as const,
goHomeDate: '2025-05-01',
receiverContactId: 'con-huber',
profilePhotoUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
},
{
...gerbil('pup-verstorben', 'Benny', 'male', '2025-03-12', 'w-kruemel', null),
status: 'Deceased' as const,
dateOfDeath: '2025-04-15',
causeOfDeath: 'Altersschwäche',
},
// UI-POLISH-1: Import-Stub ohne Namen — testet den '(ohne Namen)'-Platzhalter in Liste + Detail.
gerbil('nameless-stub', '', 'male', '2023-01-01', null, null),
// SIBLING-PAIRING: Vollgeschwister-Verpaarung (in der Zucht häufig). Beide
// Eltern von 'inzucht-kind' stammen aus DEMSELBEN Wurf 'w-zwillinge' → das
// Diagramm führt ihren Vorfahren-Ast zu einem Verweis-Knoten zusammen.
gerbil('zwilling-bock', 'Zwilling Bock', 'male', '2020-05-01', 'w-zwillinge', 'cv-agouti'),
gerbil('zwilling-maus', 'Zwilling Maus', 'female', '2020-05-01', 'w-zwillinge', 'cv-schwarz'),
{ ...gerbil('opa-w', 'Opa W', 'male', '2018-01-01', null, 'cv-agouti'), isResident: false },
{ ...gerbil('oma-u', 'Oma U', 'female', '2018-02-01', null, 'cv-gold'), isResident: false },
gerbil('inzucht-kind', 'Inzucht Kind', 'female', '2021-06-01', 'w-inzucht', 'cv-agouti'),
]
const litters: Litter[] = [
{ id: 'w-zwillinge', name: 'Wurf Z', date: '2020-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'opa-w', motherId: 'oma-u', showInChronicle: true },
{ id: 'w-inzucht', name: 'Wurf I', date: '2021-06-01', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'zwilling-bock', motherId: 'zwilling-maus', showInChronicle: true },
{
id: 'w-kruemel', name: 'Wurf K', date: '2025-03-12', totalBorn: 5, expectedGoHomeDate: '2025-04-16', notes: null, fatherId: 'fridolin', motherId: 'luna', deathsWithin8Weeks: 1, stillborn: 1, showInChronicle: true,
// NACHVERFOLGUNG: Datenherkunft des Wurfs (vom Import erzeugter JSON-String).
provenance: JSON.stringify({
sourceFiles: ['Wurfchronik Teil 1_page_0009.md', 'Wurfchronik Teil 1_page_0028.md'],
mergedRecordCount: 2,
fromWurfchronik: true,
notes: ['aus Wurfchronik', 'aus 2 Datensätzen zusammengeführt', 'Geschwister-Würfe zusammengeführt'],
history: [
'Wurf aus Wurfchronik „Wurfchronik Teil 1_page_0009.md“.',
'Auch in „Wurfchronik Teil 1_page_0028.md“ gefunden → Datensätze zusammengeführt.',
'Geschwister-Würfe zusammengeführt.',
],
}),
},
{ id: 'w-fridolin', name: 'Wurf F', date: '2023-05-01', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'balu', motherId: 'maja', showInChronicle: true },
{ id: 'w-luna', name: 'Wurf L', date: '2023-08-15', totalBorn: 6, expectedGoHomeDate: null, notes: null, fatherId: 'karlsson', motherId: 'smilla', showInChronicle: true },
{ id: 'w-balu', name: 'Wurf B', date: '2021-04-20', totalBorn: 3, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'greta', showInChronicle: true },
// Anton ist auch Karlssons Vater -> gemeinsamer Vorfahre (Inzucht-Demo)
{ id: 'w-karlsson', name: 'Wurf C', date: '2022-02-02', totalBorn: 5, expectedGoHomeDate: null, notes: null, fatherId: 'anton', motherId: 'frieda', showInChronicle: true },
{ id: 'w-anton', name: 'Wurf A', date: '2019-07-07', totalBorn: 4, expectedGoHomeDate: null, notes: null, fatherId: 'emil', motherId: 'hilde', showInChronicle: true },
{ id: 'w-emil', name: 'Wurf E', date: '2017-03-03', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'max', motherId: null, showInChronicle: true },
// WURFCHRONIK-FILTER: dieser Wurf ist NICHT in der Wurfchronik (showInChronicle: false)
// -> taucht in der Wurf-Liste nicht auf, aber auf Fridolins Tier-Detailseite (parentLitters) weiterhin.
{ id: 'w-versteckt', name: 'Wurf V', date: '2024-02-02', totalBorn: 2, expectedGoHomeDate: null, notes: null, fatherId: 'fridolin', motherId: 'luna', showInChronicle: false },
]
const enclosures: Enclosure[] = [
// Reinigung überfällig: letzte Reinigung lange her + Zyklus -> nextCleaningDate in der Vergangenheit.
{
id: 'enc-gross',
name: 'Großbecken',
notes: null,
size: '120×50 cm',
capacity: 6,
lastCleanedDate: '2020-01-01',
cleaningCycleDays: 14,
nextCleaningDate: '2020-01-15',
},
// Kein Reinigungszyklus hinterlegt.
{
id: 'enc-leer',
name: 'Quarantänebecken',
notes: null,
size: null,
capacity: null,
lastCleanedDate: null,
cleaningCycleDays: null,
nextCleaningDate: null,
},
]
// FEAT-13: contactInfo (Freitext) wurde durch strukturierte Felder ersetzt.
const contacts: Contact[] = [
{
id: 'con-meier', name: 'Zoohandlung Meier', email: 'meier@example.de', phone: null, address: 'Hauptstraße 1, 12345 Musterstadt', notes: null, isBreeder: true, isReceiver: true,
// NACHVERFOLGUNG: Datenherkunft des Kontakts (vom Import erzeugter JSON-String).
provenance: JSON.stringify({
sourceFiles: ['Wurfchronik Teil 1_page_0001.md', 'Stammbaum von Krümel.xlsx'],
mergedRecordCount: 2,
fromWurfchronik: true,
notes: ['aus 2 Datensätzen zusammengeführt', 'als Züchter erkannt', 'als Abnehmer erkannt'],
history: [
'In „Stammbaum von Krümel.xlsx“ als Züchter erkannt.',
'Auch in „Wurfchronik Teil 1_page_0001.md“ gefunden → Datensätze zusammengeführt.',
'Sowohl als Züchter als auch als Abnehmer geführt.',
],
}),
},
{ id: 'con-huber', name: 'Familie Huber', email: null, phone: '0151 2345678', address: null, notes: null, isBreeder: false, isReceiver: true },
{ id: 'con-frei', name: 'Züchterin Frei', email: null, phone: null, address: null, notes: 'unverknüpft', isBreeder: true, isReceiver: false },
{ id: 'con-neither', name: 'Weder Noch', email: null, phone: null, address: null, notes: 'weder züchter noch abnehmer', isBreeder: false, isReceiver: false },
]
const colorVarieties = [
{ id: 'cv-agouti', name: 'Agouti', canonicalGenotype: 'AA CC DD EE GG PP spsp rere', sortOrder: 1 },
{ id: 'cv-schwarz', name: 'Schwarz', canonicalGenotype: 'aa CC DD EE GG PP spsp rere', sortOrder: 2 },
{ id: 'cv-gold', name: 'Gold', canonicalGenotype: 'AA CC DD EE GG pp spsp rere', sortOrder: 3 },
{ id: 'cv-blau', name: 'Blau', canonicalGenotype: 'aa CC dd EE GG PP spsp rere', sortOrder: 4 },
{ id: 'cv-himalaya', name: 'Himalaya', canonicalGenotype: 'AA chch DD EE GG PP spsp rere', sortOrder: 5 },
{ id: 'cv-schwarz-schecke', name: 'Schwarz Schecke', canonicalGenotype: 'aa CC DD EE GG PP Spsp rere', sortOrder: 6 },
]
const healthRecords: HealthRecordRow[] = [
{
id: 'hr-1',
gerbilId: 'kruemel',
date: '2026-01-15',
type: 'Vaccination',
description: 'Jahresimpfung',
veterinarian: 'Dr. Vogel',
createdAt: '2026-01-15T10:00:00Z',
},
]
const weightRecords: WeightRecordRow[] = [
{ id: 'wr-1', gerbilId: 'kruemel', date: '2026-05-01', weightGrams: 78, notes: null },
{ id: 'wr-2', gerbilId: 'kruemel', date: '2026-05-20', weightGrams: 82, notes: null },
]
// WEB-0b: die 6 Webseiten-Seiten (wie vom Backend geseedet), mit ein paar Blöcken.
const pages: MockPage[] = [
{
id: 'page-start',
slug: 'start',
title: 'Startseite',
seoDescription: 'Willkommen bei unserer Rennmauszucht.',
status: 'Published',
blocks: [
{ id: 'blk-start-1', order: 0, type: 'Heading', data: { text: 'Willkommen', level: 2 } },
{ id: 'blk-start-2', order: 1, type: 'RichText', data: { markdown: 'Schön, dass du da bist.' } },
],
},
{ id: 'page-zucht', slug: 'ueber-die-zucht', title: 'Über die Zucht', seoDescription: null, status: 'Draft', blocks: [] },
{
id: 'page-abgabe',
slug: 'abgabetiere',
title: 'Abgabetiere',
seoDescription: null,
status: 'Published',
blocks: [
{ id: 'blk-abgabe-1', order: 0, type: 'AbgabetiereList', data: { mode: 'auto', intro: 'Diese Tiere suchen ein Zuhause.' } },
],
},
{ id: 'page-bedingungen', slug: 'abgabebedingungen', title: 'Abgabebedingungen', seoDescription: null, status: 'Draft', blocks: [] },
{ id: 'page-farben', slug: 'farben-genetik', title: 'Farben & Genetik', seoDescription: null, status: 'Draft', blocks: [] },
{ id: 'page-kontakt', slug: 'kontakt', title: 'Kontakt', seoDescription: null, status: 'Draft', blocks: [] },
]
// INBOX-1: Anfragen (neueste zuerst nach receivedAt sortierbar)
const requests: InboxRequest[] = [
{
id: 'req-anna',
gmailMessageId: '<anna-1@mail.example>',
threadId: 'thr-1',
fromAddress: 'anna@example.de',
fromName: 'Anna Albrecht',
subject: 'Anfrage: Pärchen zur Abgabe?',
bodyText:
'Hallo,\n\nich habe euer Inserat gesehen — sind die beiden Agouti-Jungs noch zu haben?\n\nViele Grüße\nAnna',
receivedAt: '2026-06-05T18:30:00Z',
status: 'New',
assignedContactId: null,
draftReply: null,
answeredAt: null,
},
{
id: 'req-ben',
gmailMessageId: '<ben-1@mail.example>',
threadId: 'thr-2',
fromAddress: 'ben@example.org',
fromName: 'Ben Berger',
subject: 'Frage zur Haltung',
bodyText: 'Guten Tag, was für ein Becken empfehlt ihr für zwei Rennmäuse?',
receivedAt: '2026-06-04T09:00:00Z',
status: 'InProgress',
assignedContactId: null,
draftReply: 'Hallo Ben, wir empfehlen mindestens 100×50 cm …',
answeredAt: null,
},
{
id: 'req-clara',
gmailMessageId: '<clara-1@mail.example>',
threadId: null,
fromAddress: 'clara@example.com',
fromName: null,
subject: 'Danke!',
bodyText: 'Die beiden sind super angekommen — danke nochmal!',
receivedAt: '2026-06-01T12:00:00Z',
status: 'Answered',
assignedContactId: 'con-meier',
draftReply: null,
answeredAt: '2026-06-02T08:00:00Z',
},
]
return {
gerbils,
litters,
enclosures,
contacts,
colorVarieties,
healthRecords,
weightRecords,
pages,
requests,
aiConfigured: true,
mailConfigured: true,
// Ein importierter Vertrag (hasFile:false) prüft die Word-Button-Ausblendung.
contracts: [
{
id: 'contract-import-1',
contactId: 'con-meier',
price: 30,
handoverDate: '2023-09-29',
contractDate: '2023-09-29',
fileName: 'Zucht der kleinen Chaoten _ (Krümel) - Zoohandlung Meier_.docx',
createdAt: '2023-09-29T00:00:00Z',
gerbilIds: ['kruemel'],
url: '/contracts/contract-import-1/file',
hasFile: false,
},
],
saleAdConfigured: true,
namesConfigured: true,
// GEPRÜFTE TIERE: startet leer; die Verify-Specs markieren über POST.
verified: [],
// FEEDBACK-TICKETS: ein offenes + ein gelöstes Ticket (GET liefert neueste zuerst →
// db.feedback wird im Mock umgekehrt; das offene steht hier zuletzt = erscheint oben).
feedback: [
{
id: 'feedback-resolved',
message: 'Der Stammbaum zeigt den falschen Vater bei Krümel.',
context: 'stammbaum',
gerbilId: 'kruemel',
litterId: null,
contactId: null,
entityName: 'Krümel',
url: 'http://localhost:5173/rennmaeuse/kruemel/stammbaum',
clientTimestamp: '2026-06-10T09:00:00Z',
userAgent: null,
createdAt: '2026-06-10T09:00:00Z',
status: 'Resolved',
category: 'Stammbaum',
resolvedAt: '2026-06-12T14:00:00Z',
question: null,
answer: null,
answeredAt: null,
// Changelog (laienverständlich) für die geschlossene Ansicht.
fixNote: 'Der Vater von Krümel war falsch — er ist jetzt Balu. Bitte den Stammbaum neu laden.',
// INTERN: darf NIE in der Züchterin-UI erscheinen.
agentContext: 'GEHEIM-AGENT-CONTEXT: motherId via conflict-decisions.json gesetzt, re-ingest.',
thread: [],
},
{
// Ticket mit offener Rückfrage (NeedsInfo): die Züchterin soll hier antworten können.
// Hat bereits eine frühere, beantwortete Runde im Verlauf (thread).
id: 'feedback-needsinfo',
message: 'Der Wurf B hat ein falsches Geburtsdatum.',
context: 'litter-detail',
gerbilId: null,
litterId: 'wurf-b',
contactId: null,
entityName: 'Wurf B',
url: 'http://localhost:5173/wuerfe/wurf-b',
clientTimestamp: '2026-06-14T08:00:00Z',
userAgent: null,
createdAt: '2026-06-14T08:00:00Z',
status: 'NeedsInfo',
resolvedAt: null,
question:
'Welches Geburtsdatum ist korrekt — der **1.** oder der **2. Juni**?\n\nBitte wähle:\n- 1. Juni\n- 2. Juni',
answer: null,
answeredAt: null,
fixNote: null,
agentContext: 'GEHEIM-AGENT-CONTEXT: warte auf DOB-Bestätigung für Wurf B.',
// Frühere, abgeschlossene Runde (älteste zuerst) — bleibt im Verlauf erhalten.
thread: [
{ role: 'maintainer', text: 'Wurde der Wurf im Mai oder Juni geboren?', at: '2026-06-13T08:00:00Z' },
{ role: 'breeder', text: 'Im Juni.', at: '2026-06-13T18:00:00Z' },
],
},
{
// Beantwortetes Ticket (Answered): die KI hat noch NICHT reagiert. Der Erst-Text ist
// jetzt gesperrt (kein „Bearbeiten"), nur die neueste Antwort ist noch änderbar.
id: 'feedback-answered',
message: 'Bei Karlsson stimmt der Farbschlag nicht.',
context: 'gerbil-detail',
gerbilId: 'karlsson',
litterId: null,
contactId: null,
entityName: 'Karlsson',
url: 'http://localhost:5173/rennmaeuse/karlsson',
clientTimestamp: '2026-06-16T09:00:00Z',
userAgent: null,
createdAt: '2026-06-16T09:00:00Z',
status: 'Answered',
resolvedAt: null,
question: 'Welcher Farbschlag ist korrekt?',
answer: 'Karlsson ist Blau, nicht Schwarz.',
answeredAt: '2026-06-16T18:00:00Z',
fixNote: null,
agentContext: null,
thread: [],
},
{
id: 'feedback-open',
message: 'Das Gewicht wird auf der Verlaufskurve falsch gerundet.',
context: 'gerbil-detail',
gerbilId: 'fridolin',
litterId: null,
contactId: null,
entityName: 'Fridolin',
url: 'http://localhost:5173/rennmaeuse/fridolin',
clientTimestamp: '2026-06-15T11:30:00Z',
userAgent: null,
createdAt: '2026-06-15T11:30:00Z',
status: 'Open',
resolvedAt: null,
question: null,
answer: null,
answeredAt: null,
fixNote: null,
agentContext: null,
thread: [],
},
],
rpro3: {
analyze: {
counts: {
ownAnimals: 303,
externalRaw: 7427,
externalAfterDedup: 3391,
litters: 127,
contacts: 750,
genotypes: 7732,
duplicatesMerged: 4036,
mergeClusters: 782,
},
newVsCurrentNew: 280,
newVsCurrentExisting: 23,
topMerges: [
{ name: 'Alice', recordCount: 30, dob: '2010-07-19', farbe: 'Schimmel', origin: 'Clan of Kleine Füchse' },
{ name: 'BlackEye', recordCount: 28, dob: '', farbe: 'Silberschimmel', origin: 'Clan of Desert' },
],
ambiguousNames: [
{
name: 'Merlin',
bareCount: 0,
variants: [
{ count: 4, dob: '2007-08-22', farbe: 'Blaufuchs', origin: 'Clan of the Yankees', isOwn: false },
{ count: 3, dob: '—', farbe: 'Platin-Schecke', origin: '—', isOwn: false },
],
},
{
name: 'Bella',
bareCount: 0,
variants: [
{ count: 2, dob: '2012-06-21', farbe: 'Blaufuchs', origin: 'Clan of Sunset Glow', isOwn: false },
{ count: 1, dob: '2013-03-20', farbe: 'Algierfuchs, hell', origin: 'eigene Zucht', isOwn: true },
],
},
],
photosProvided: false,
photoFilesAvailable: 0,
},
execute: {
gerbilsImported: 3694,
littersImported: 1678,
contactsImported: 508,
healthRecordsImported: 39,
weightRecordsImported: 229,
photosImported: 0,
message: 'Import erfolgreich: 3694 Tiere, 1678 Würfe, 508 Kontakte.',
},
},
acquisitions: [],
// ABGABE-STATUS: eine reservierte + eine abgegebene + eine verfügbare Vormerkung.
reservations: [
{
id: 'res-reserviert',
gerbilId: 'sale-balu',
gerbilName: 'Balu Abgabe',
status: 'reserviert',
reservedForContactId: 'con-huber',
contactName: 'Familie Huber',
appointmentDate: '2026-07-01T00:00:00Z',
price: 25,
note: 'Käfig wird mitgebracht.',
handedOverDate: null,
createdAt: '2026-06-15T09:00:00Z',
updatedAt: '2026-06-15T09:00:00Z',
},
{
id: 'res-abgegeben',
gerbilId: 'pup-abgabe',
gerbilName: 'Pippa',
status: 'abgegeben',
reservedForContactId: 'con-huber',
contactName: 'Familie Huber',
appointmentDate: null,
price: 20,
note: null,
handedOverDate: '2025-05-01T00:00:00Z',
createdAt: '2025-04-20T09:00:00Z',
updatedAt: '2025-05-01T09:00:00Z',
},
{
id: 'res-verfuegbar',
gerbilId: 'sale-benny',
gerbilName: 'Benny Abgabe',
status: 'verfuegbar',
reservedForContactId: null,
contactName: null,
appointmentDate: null,
price: null,
note: null,
handedOverDate: null,
createdAt: '2026-06-16T09:00:00Z',
updatedAt: '2026-06-16T09:00:00Z',
},
],
waitingList: [
{
id: 'wl-seed-1',
contactId: contacts[0]?.id ?? null,
contactName: contacts[0]?.name ?? 'Familie Sonntag',
wishColor: 'Schwarz',
wishGender: 'female',
requestedAt: '2026-05-01T00:00:00Z',
status: 'offen',
note: 'möchte zwei Weibchen',
createdAt: '2026-05-01T00:00:00Z',
},
{
id: 'wl-seed-2',
contactId: null,
contactName: 'Herr Maier',
wishColor: null,
wishGender: 'male',
requestedAt: '2026-04-10T00:00:00Z',
status: 'erfuellt',
note: null,
createdAt: '2026-04-10T00:00:00Z',
},
],
returns: [],
exhibitions: [],
}
}