- WinterCheck per colony, winter and season (autumn/spring), every field editable; loss causes limited to the COLOSS monitoring categories. - Summary per winter and apiary: loss rate lost/wintered, marked provisional while colonies are not yet checked in spring - an unchecked colony never counts as survived. - A loss marks the colony deceased only on explicit request, in the same transaction and audited. - Permissions winter-checks:read/create/update granted by migration to beekeeper and staff roles; winter checks included in backups. - Page "Überwinterung": next action first, one button per row until the colony is wintered, German number format. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1283 lines
40 KiB
Plaintext
1283 lines
40 KiB
Plaintext
// ApiCore MVP-Datenmodell – abgeleitet aus docs/lastenheft.md Abschnitt 13, 4.2-4.22, 26.
|
||
// Prinzip: Historie statt Löschen (siehe ARCHITECTURE.md §4) – fachliche Haupttabellen
|
||
// tragen `status` + `deactivatedAt` statt Hard-Delete. Beziehungen sind relationale FKs,
|
||
// keine Freitextfelder, damit die Rückverfolgbarkeitskette (Abschnitt 16, 29) abfragbar bleibt.
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
output = "../generated/prisma"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Benutzer / Auth / Audit (Abschnitt 3, 14, 15, 16)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum RoleStatus {
|
||
ACTIVE
|
||
INACTIVE
|
||
}
|
||
|
||
model Role {
|
||
id String @id @default(cuid())
|
||
key String @unique
|
||
name String
|
||
description String?
|
||
isSystem Boolean @default(false)
|
||
status RoleStatus @default(ACTIVE)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
permissions RolePermission[]
|
||
users User[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
model Permission {
|
||
id String @id @default(cuid())
|
||
key String @unique
|
||
resource String
|
||
action String
|
||
labelDe String
|
||
groupDe String
|
||
sortOrder Int
|
||
|
||
roles RolePermission[]
|
||
|
||
@@unique([resource, action])
|
||
@@index([resource])
|
||
}
|
||
|
||
model RolePermission {
|
||
roleId String
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
permissionId String
|
||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||
grantedAt DateTime @default(now())
|
||
|
||
@@id([roleId, permissionId])
|
||
@@index([permissionId])
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
passwordHash String
|
||
name String
|
||
roleId String
|
||
role Role @relation(fields: [roleId], references: [id])
|
||
isActive Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
// TOTP-Secret AES-256-GCM-verschluesselt gespeichert (siehe
|
||
// src/auth/mfa/mfa-crypto.util.ts), nie im Klartext in der DB.
|
||
mfaEnabled Boolean @default(false)
|
||
mfaSecret String?
|
||
avatarPath String?
|
||
|
||
// Gesetzt bei jedem Passwortwechsel; JwtAuthGuard verwirft Access-Tokens,
|
||
// die vor diesem Zeitpunkt ausgestellt wurden (siehe
|
||
// PermissionResolverService.isTokenValid), damit ein gestohlener Token
|
||
// nach einer Passwortaenderung nicht bis zu 15 Minuten weiter funktioniert.
|
||
passwordChangedAt DateTime?
|
||
|
||
refreshTokens RefreshToken[]
|
||
|
||
inspections Inspection[]
|
||
varroaMeasurements VarroaMeasurement[]
|
||
swarmControlChecks SwarmControlCheck[]
|
||
winterChecks WinterCheck[]
|
||
treatments Treatment[]
|
||
feedings Feeding[]
|
||
harvests Harvest[]
|
||
extractions Extraction[]
|
||
honeyMeasurements HoneyMeasurement[]
|
||
stockMovements StockMovement[]
|
||
assignedTasks Task[] @relation("TaskAssignee")
|
||
createdTasks Task[] @relation("TaskCreator")
|
||
assignedTaskTemplates TaskTemplate[] @relation("TaskTemplateAssignee")
|
||
createdTaskTemplates TaskTemplate[] @relation("TaskTemplateCreator")
|
||
colonyMoves ColonyApiaryHistory[]
|
||
auditLogs AuditLog[]
|
||
bottlingBatches BottlingBatch[]
|
||
notifications Notification[]
|
||
queenEvaluations QueenEvaluation[]
|
||
webhookSubscriptions WebhookSubscription[]
|
||
documents Document[]
|
||
honeyBatchRelocations HoneyBatchRelocation[]
|
||
apiKeys ApiKey[]
|
||
|
||
@@index([roleId])
|
||
}
|
||
|
||
// Schlüssel für Maschinen-Zugriffe (z. B. Wabenhain-Shop-Sync) statt eines
|
||
// Service-Kontos mit Passwort. Gespeichert wird nur der SHA-256-Hash des
|
||
// Schlüssels; der Klartext wird genau einmal bei der Erstellung angezeigt.
|
||
// Ein Schlüssel ist kein Benutzer: er darf nur die Routen, deren
|
||
// Berechtigung in `scopes` steht (siehe api-keys/api-key-scopes.ts).
|
||
model ApiKey {
|
||
id String @id @default(cuid())
|
||
name String
|
||
// Anfang des Schlüssels ("ack_ab12cd34") - zum Wiedererkennen in der
|
||
// Liste, ohne den Schlüssel selbst zu speichern.
|
||
prefix String
|
||
keyHash String @unique
|
||
scopes String[]
|
||
createdByUserId String?
|
||
createdByUser User? @relation(fields: [createdByUserId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
lastUsedAt DateTime?
|
||
expiresAt DateTime?
|
||
revokedAt DateTime?
|
||
|
||
@@index([revokedAt])
|
||
}
|
||
|
||
model RefreshToken {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
tokenHash String @unique
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
revokedAt DateTime?
|
||
|
||
@@index([userId])
|
||
}
|
||
|
||
enum AuditAction {
|
||
CREATE
|
||
UPDATE
|
||
DEACTIVATE
|
||
DELETE
|
||
LOGIN
|
||
LOGIN_FAILED
|
||
LOGOUT
|
||
TOKEN_REFRESHED
|
||
MFA_ENABLED
|
||
MFA_DISABLED
|
||
}
|
||
|
||
model AuditLog {
|
||
id String @id @default(cuid())
|
||
entityType String
|
||
entityId String
|
||
action AuditAction
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
timestamp DateTime @default(now())
|
||
beforeData Json?
|
||
afterData Json?
|
||
|
||
@@index([entityType, entityId])
|
||
@@index([timestamp])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Bienenstände (Abschnitt 4.2)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ApiaryStatus {
|
||
ACTIVE
|
||
INACTIVE
|
||
}
|
||
|
||
model Apiary {
|
||
id String @id @default(cuid())
|
||
name String
|
||
description String?
|
||
location String?
|
||
latitude Float?
|
||
longitude Float?
|
||
address String?
|
||
status ApiaryStatus @default(ACTIVE)
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
colonies Colony[]
|
||
colonyMoves ColonyApiaryHistory[]
|
||
harvests Harvest[]
|
||
treatments Treatment[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Völkerverwaltung (Abschnitt 4.3, 4.4)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ColonyType {
|
||
PRODUCTION_COLONY
|
||
NUCLEUS_COLONY
|
||
YOUNG_COLONY
|
||
CARETAKER_COLONY
|
||
MATING_NUCLEUS
|
||
RESERVE_COLONY
|
||
}
|
||
|
||
enum ColonyStatus {
|
||
ACTIVE
|
||
WEAK
|
||
CONSPICUOUS
|
||
QUEENLESS
|
||
DISSOLVED
|
||
SOLD
|
||
DECEASED
|
||
MERGED
|
||
}
|
||
|
||
// Fuenfstufige Zustandsskala, geteilt von Brut-, Futter- und Gesundheitsstatus.
|
||
enum ColonyCondition {
|
||
EXCELLENT
|
||
GOOD
|
||
AVERAGE
|
||
WEAK
|
||
CRITICAL
|
||
}
|
||
|
||
model Colony {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
hiveType String?
|
||
originText String?
|
||
colonyType ColonyType
|
||
status ColonyStatus @default(ACTIVE)
|
||
strength Int?
|
||
broodStatus ColonyCondition?
|
||
feedStatus ColonyCondition?
|
||
healthStatus ColonyCondition?
|
||
notes String?
|
||
currentQueenId String? @unique
|
||
currentQueen Queen? @relation("ColonyCurrentQueen", fields: [currentQueenId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
apiaryHistory ColonyApiaryHistory[]
|
||
queenHistory QueenColonyHistory[]
|
||
lineageAsSource ColonyLineage[] @relation("LineageFrom")
|
||
lineageAsResult ColonyLineage[] @relation("LineageTo")
|
||
inspections Inspection[]
|
||
varroaMeasurements VarroaMeasurement[]
|
||
swarmControlChecks SwarmControlCheck[]
|
||
winterChecks WinterCheck[]
|
||
treatments Treatment[]
|
||
feedings Feeding[]
|
||
harvests HarvestColony[]
|
||
queenEvaluations QueenEvaluation[]
|
||
|
||
@@index([apiaryId])
|
||
@@index([status])
|
||
}
|
||
|
||
// Verschiebung Volk <-> Bienenstand, historisch nachvollziehbar (Abschnitt 4.2)
|
||
model ColonyApiaryHistory {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
movedAt DateTime @default(now())
|
||
movedByUserId String?
|
||
movedByUser User? @relation(fields: [movedByUserId], references: [id])
|
||
notes String?
|
||
|
||
@@index([colonyId])
|
||
@@index([apiaryId])
|
||
}
|
||
|
||
enum LineageType {
|
||
SPLIT
|
||
MERGE
|
||
}
|
||
|
||
// Ableger bilden (SPLIT: from=Muttervolk, to=Ableger) und Völker vereinigen
|
||
// (MERGE: mehrere from-Zeilen auf denselben to=Zielvolk) – Abschnitt 4.4.
|
||
model ColonyLineage {
|
||
id String @id @default(cuid())
|
||
type LineageType
|
||
fromColonyId String
|
||
fromColony Colony @relation("LineageFrom", fields: [fromColonyId], references: [id])
|
||
toColonyId String
|
||
toColony Colony @relation("LineageTo", fields: [toColonyId], references: [id])
|
||
date DateTime
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([fromColonyId])
|
||
@@index([toColonyId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Königinnenverwaltung (Abschnitt 4.5)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum QueenStatus {
|
||
ACTIVE
|
||
RESERVE
|
||
UNMATED
|
||
MATED
|
||
LOST
|
||
REPLACED
|
||
SOLD
|
||
DECEASED
|
||
}
|
||
|
||
model Queen {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
hatchYear Int?
|
||
breed String?
|
||
origin String?
|
||
breeder String?
|
||
markingColor String?
|
||
markingNumber String?
|
||
matingType String?
|
||
matingStatus String?
|
||
inseminationData String?
|
||
motherLine String?
|
||
fatherLine String?
|
||
motherQueenId String?
|
||
status QueenStatus @default(ACTIVE)
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deactivatedAt DateTime?
|
||
|
||
motherQueen Queen? @relation("QueenMotherline", fields: [motherQueenId], references: [id])
|
||
daughterQueens Queen[] @relation("QueenMotherline")
|
||
colony Colony? @relation("ColonyCurrentQueen")
|
||
colonyHistory QueenColonyHistory[]
|
||
evaluations QueenEvaluation[]
|
||
|
||
@@index([status])
|
||
@@index([motherQueenId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Königinnenbewertung (Abschnitt 4.7) – v1.x Modul 31
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model QueenEvaluation {
|
||
id String @id @default(cuid())
|
||
queenId String
|
||
queen Queen @relation(fields: [queenId], references: [id])
|
||
colonyId String?
|
||
colony Colony? @relation(fields: [colonyId], references: [id])
|
||
date DateTime
|
||
colonyStrength Int
|
||
gentleness Int
|
||
combBuild Int
|
||
swarmingTendency Int
|
||
broodPattern Int
|
||
honeyYield Int
|
||
notes String?
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([queenId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
// Königinnenwechsel historisch nachvollziehbar (Abschnitt 4.5)
|
||
model QueenColonyHistory {
|
||
id String @id @default(cuid())
|
||
queenId String
|
||
queen Queen @relation(fields: [queenId], references: [id])
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
startDate DateTime
|
||
endDate DateTime?
|
||
notes String?
|
||
|
||
@@index([queenId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Durchsichten / digitale Stockkarte (Abschnitt 4.6, 4.7)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Inspection {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
weather String?
|
||
temperature Float?
|
||
colonyStrength Int?
|
||
broodAmount String?
|
||
broodPattern Int?
|
||
openBroodPresent Boolean?
|
||
cappedBroodPresent Boolean?
|
||
eggsPresent Boolean?
|
||
queenSeen Boolean?
|
||
queenCellsPresent Boolean?
|
||
swarmMood Int?
|
||
gentleness Int?
|
||
combBehavior Int?
|
||
feedStatus String?
|
||
honeySuperStatus String?
|
||
healthIssues String?
|
||
varroaIssues String?
|
||
measures String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
tasks Task[]
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Aufgabenmanagement (Abschnitt 4.8)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum TaskPriority {
|
||
LOW
|
||
NORMAL
|
||
HIGH
|
||
URGENT
|
||
}
|
||
|
||
enum TaskStatus {
|
||
OPEN
|
||
IN_PROGRESS
|
||
DONE
|
||
CANCELLED
|
||
}
|
||
|
||
enum TaskRelatedType {
|
||
COLONY
|
||
APIARY
|
||
QUEEN
|
||
HONEY_BATCH
|
||
STORAGE
|
||
GENERAL
|
||
}
|
||
|
||
model Task {
|
||
id String @id @default(cuid())
|
||
title String
|
||
description String?
|
||
priority TaskPriority @default(NORMAL)
|
||
dueDate DateTime?
|
||
status TaskStatus @default(OPEN)
|
||
assignedUserId String?
|
||
assignedUser User? @relation("TaskAssignee", fields: [assignedUserId], references: [id])
|
||
createdByUserId String?
|
||
createdByUser User? @relation("TaskCreator", fields: [createdByUserId], references: [id])
|
||
relatedType TaskRelatedType @default(GENERAL)
|
||
relatedId String?
|
||
sourceInspectionId String?
|
||
sourceInspection Inspection? @relation(fields: [sourceInspectionId], references: [id])
|
||
sourceTemplateId String?
|
||
sourceTemplate TaskTemplate? @relation(fields: [sourceTemplateId], references: [id])
|
||
// Set when an automation rule created the task (not a person). Keeps an
|
||
// automatic suggestion distinguishable from a planned task, and lets the
|
||
// rule see that it already left an open task for the same object instead
|
||
// of piling up a duplicate on every further trigger.
|
||
sourceAutomationTrigger AutomationTriggerType?
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
|
||
@@index([status, dueDate])
|
||
@@index([relatedType, relatedId])
|
||
@@index([sourceAutomationTrigger, relatedType, relatedId, status])
|
||
}
|
||
|
||
enum TaskTemplateRecurrenceType {
|
||
YEARLY
|
||
MONTHLY
|
||
INTERVAL_DAYS
|
||
}
|
||
|
||
// Wiederkehrende Aufgaben-Vorlagen – erzeugen automatisch eine neue Task,
|
||
// sobald nextDueDate erreicht ist (siehe TasksService.generateDueTasks).
|
||
model TaskTemplate {
|
||
id String @id @default(cuid())
|
||
title String
|
||
description String?
|
||
priority TaskPriority @default(NORMAL)
|
||
assignedUserId String?
|
||
assignedUser User? @relation("TaskTemplateAssignee", fields: [assignedUserId], references: [id])
|
||
createdByUserId String?
|
||
createdByUser User? @relation("TaskTemplateCreator", fields: [createdByUserId], references: [id])
|
||
recurrenceType TaskTemplateRecurrenceType
|
||
recurrenceMonth Int?
|
||
recurrenceDayOfMonth Int?
|
||
intervalDays Int?
|
||
nextDueDate DateTime
|
||
active Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
generatedTasks Task[]
|
||
|
||
@@index([active, nextDueDate])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Varroamanagement (Abschnitt 4.10)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum VarroaMethod {
|
||
NATURAL_MITE_FALL
|
||
POWDERED_SUGAR
|
||
WASHING_METHOD
|
||
DRONE_BROOD_CONTROL
|
||
}
|
||
|
||
model VarroaMeasurement {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
method VarroaMethod
|
||
durationMinutes Int?
|
||
miteCount Int?
|
||
calculatedValue Float?
|
||
assessment String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Schwarmkontrolle (jenseits Lastenheft: strukturiertes Protokoll für
|
||
// Weiselzellenkontrollen während der Schwarmzeit)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum SwarmMood {
|
||
NONE
|
||
LOW
|
||
HIGH
|
||
}
|
||
|
||
enum SwarmControlAction {
|
||
NONE
|
||
QUEEN_CELLS_REMOVED
|
||
ARTIFICIAL_SWARM
|
||
COLONY_DIVIDED
|
||
OTHER
|
||
}
|
||
|
||
model SwarmControlCheck {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
queenCellsFound Int?
|
||
queenCellsCapped Boolean @default(false)
|
||
swarmMood SwarmMood @default(NONE)
|
||
actionTaken SwarmControlAction @default(NONE)
|
||
notes String?
|
||
nextCheckDate DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Ein- und Auswinterung, Winterverluste
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum WinterCheckSeason {
|
||
AUTUMN // Einwinterung
|
||
SPRING // Auswinterung
|
||
}
|
||
|
||
enum WinterOutcome {
|
||
SURVIVED
|
||
WEAK
|
||
LOST
|
||
}
|
||
|
||
// Verlustkategorien des COLOSS-Monitorings: totes/leeres Volk, unloesbares
|
||
// Koeniginnenproblem, Naturereignis. Bewusst keine Diagnose (Varroa, Virus
|
||
// ...): die gehoert, wenn ueberhaupt bekannt, in die Notiz.
|
||
enum ColonyLossCause {
|
||
DEAD_OR_EMPTY
|
||
QUEEN_PROBLEM
|
||
NATURAL_DISASTER
|
||
}
|
||
|
||
// Eine Einwinterung und eine Auswinterung je Volk und Winter. winterYear ist
|
||
// das Jahr, in dem der Winter beginnt (Winter 2026/27 = 2026).
|
||
model WinterCheck {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
winterYear Int
|
||
season WinterCheckSeason
|
||
date DateTime
|
||
occupiedSeams Int?
|
||
feedStockKg Float?
|
||
queenPresent Boolean?
|
||
outcome WinterOutcome?
|
||
lossCause ColonyLossCause?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([colonyId, winterYear, season])
|
||
@@index([winterYear])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Behandlung / Tierarzneimittel (Abschnitt 4.11)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Treatment {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
startDate DateTime?
|
||
endDate DateTime?
|
||
product String
|
||
activeIngredient String?
|
||
amount String?
|
||
method String?
|
||
reason String?
|
||
batchNumber String?
|
||
notes String?
|
||
withdrawalPeriodDays Int?
|
||
// Bestandsbuch (Art. 108 VO (EU) 2019/6): where the colony stood on the
|
||
// day of the treatment - kept with the treatment because the colony may
|
||
// be moved later - and the supplier / purchase proof / prescribing vet.
|
||
apiaryId String?
|
||
apiary Apiary? @relation(fields: [apiaryId], references: [id])
|
||
supplier String?
|
||
purchaseProof String?
|
||
prescribingVet String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Fütterung (Abschnitt 4.12)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum FeedType {
|
||
SUGAR_WATER
|
||
FEED_SYRUP
|
||
FEED_PASTE
|
||
OWN_HONEY
|
||
OTHER
|
||
}
|
||
|
||
model Feeding {
|
||
id String @id @default(cuid())
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
feedType FeedType
|
||
product String?
|
||
amount Float
|
||
unit String
|
||
batchNumber String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([colonyId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Ernte / Schleuderung / Honigcharge / Rückverfolgbarkeit (Abschnitt 4.13-4.17)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model Harvest {
|
||
id String @id @default(cuid())
|
||
apiaryId String
|
||
apiary Apiary @relation(fields: [apiaryId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
date DateTime
|
||
honeyType String?
|
||
honeySuperCount Int?
|
||
grossWeight Float?
|
||
netWeight Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
colonies HarvestColony[]
|
||
extractions ExtractionHarvest[]
|
||
|
||
@@index([apiaryId, date])
|
||
}
|
||
|
||
model HarvestColony {
|
||
id String @id @default(cuid())
|
||
harvestId String
|
||
harvest Harvest @relation(fields: [harvestId], references: [id])
|
||
colonyId String
|
||
colony Colony @relation(fields: [colonyId], references: [id])
|
||
|
||
@@unique([harvestId, colonyId])
|
||
@@index([colonyId])
|
||
}
|
||
|
||
model Extraction {
|
||
id String @id @default(cuid())
|
||
date DateTime
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
amount Float?
|
||
room String?
|
||
waterContent Float?
|
||
temperature Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
harvests ExtractionHarvest[]
|
||
honeyBatches HoneyBatch[]
|
||
|
||
@@index([date])
|
||
}
|
||
|
||
model ExtractionHarvest {
|
||
id String @id @default(cuid())
|
||
extractionId String
|
||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||
harvestId String
|
||
harvest Harvest @relation(fields: [harvestId], references: [id])
|
||
|
||
@@unique([extractionId, harvestId])
|
||
@@index([harvestId])
|
||
}
|
||
|
||
enum HoneyBatchStatus {
|
||
RAW
|
||
EXTRACTED
|
||
CLARIFIED
|
||
CREAMED
|
||
STORED
|
||
BOTTLED
|
||
SOLD
|
||
CONSUMED
|
||
}
|
||
|
||
model HoneyBatch {
|
||
id String @id @default(cuid())
|
||
batchNumber String @unique
|
||
extractionId String
|
||
extraction Extraction @relation(fields: [extractionId], references: [id])
|
||
honeyType String?
|
||
amount Float?
|
||
waterContent Float?
|
||
storageContainerId String?
|
||
storageContainer StorageContainer? @relation(fields: [storageContainerId], references: [id])
|
||
status HoneyBatchStatus @default(RAW)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
measurements HoneyMeasurement[]
|
||
bottlingBatches BottlingBatch[]
|
||
relocations HoneyBatchRelocation[]
|
||
containerAllocations HoneyBatchContainerAllocation[]
|
||
|
||
@@index([status])
|
||
@@index([extractionId])
|
||
}
|
||
|
||
// Honigmessungen (Abschnitt 4.17) – als Teil des HoneyBatch-Moduls im MVP,
|
||
// siehe ARCHITECTURE.md Annahme 1.
|
||
model HoneyMeasurement {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
waterContent Float
|
||
measuredAt DateTime
|
||
device String?
|
||
temperature Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Lagerbehälter / grundlegende Lagerverwaltung (Abschnitt 4.18, 4.21)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum StorageContainerStatus {
|
||
EMPTY
|
||
FILLED
|
||
CLEANING
|
||
OUT_OF_SERVICE
|
||
}
|
||
|
||
model StorageContainer {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
containerType String?
|
||
capacity Float?
|
||
currentAmount Float?
|
||
location String?
|
||
status StorageContainerStatus @default(EMPTY)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
honeyBatches HoneyBatch[]
|
||
relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom")
|
||
relocationsTo HoneyBatchRelocation[] @relation("RelocationTo")
|
||
batchAllocations HoneyBatchContainerAllocation[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// Umlagerungshistorie einer Honigcharge zwischen Lagerbehältern (Abschnitt 26, v1.x Modul 32)
|
||
model HoneyBatchRelocation {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
fromStorageContainerId String?
|
||
fromStorageContainer StorageContainer? @relation("RelocationFrom", fields: [fromStorageContainerId], references: [id])
|
||
toStorageContainerId String
|
||
toStorageContainer StorageContainer @relation("RelocationTo", fields: [toStorageContainerId], references: [id])
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
notes String?
|
||
relocatedAt DateTime @default(now())
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId, relocatedAt])
|
||
}
|
||
|
||
// Aufteilung einer Honigcharge auf mehrere Lagerbehälter (z.B. 125 kg auf 5
|
||
// Eimer à 25 kg) - additiv zum obigen "primären" storageContainerId/
|
||
// Umlagern-Fluss auf HoneyBatch, der weiterhin den einfachen Ein-Behälter-
|
||
// Fall abdeckt; diese Tabelle bildet zusätzlich den Fall ab, dass eine
|
||
// Charge zu groß für einen einzelnen Behälter ist. @@unique macht das
|
||
// Zuweisen einer Menge zu einem Behälter ein Upsert (erneutes Zuweisen
|
||
// überschreibt die Menge, statt einen Duplikat-Eintrag anzulegen).
|
||
model HoneyBatchContainerAllocation {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
storageContainerId String
|
||
storageContainer StorageContainer @relation(fields: [storageContainerId], references: [id])
|
||
amount Float
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([honeyBatchId, storageContainerId])
|
||
@@index([storageContainerId])
|
||
}
|
||
|
||
enum StockCategory {
|
||
HONEY
|
||
CONSUMABLE
|
||
EQUIPMENT_MATERIAL
|
||
}
|
||
|
||
model StockItem {
|
||
id String @id @default(cuid())
|
||
name String
|
||
category StockCategory
|
||
unit String
|
||
quantity Float @default(0)
|
||
minThreshold Float?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
movements StockMovement[]
|
||
|
||
@@index([category])
|
||
}
|
||
|
||
enum StockMovementType {
|
||
IN
|
||
OUT
|
||
TRANSFER
|
||
ADJUSTMENT
|
||
}
|
||
|
||
model StockMovement {
|
||
id String @id @default(cuid())
|
||
stockItemId String
|
||
stockItem StockItem @relation(fields: [stockItemId], references: [id])
|
||
type StockMovementType
|
||
quantity Float
|
||
date DateTime @default(now())
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
relatedType String?
|
||
relatedId String?
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([stockItemId, date])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Abfüllungen und Produkte (Abschnitt 4.19, 4.20) – v1.x Modul 23
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum ProductStatus {
|
||
ACTIVE
|
||
DISCONTINUED
|
||
}
|
||
|
||
model Product {
|
||
id String @id @default(cuid())
|
||
name String
|
||
description String?
|
||
sku String? @unique
|
||
packagingSize Float?
|
||
weight Float?
|
||
salesUnit String?
|
||
status ProductStatus @default(ACTIVE)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
bottlingBatches BottlingBatch[]
|
||
|
||
@@index([status])
|
||
}
|
||
|
||
// Etikettenvorlage mit {{Platzhalter}}-Text, wird beim Drucken mit den
|
||
// Daten einer Abfuellung befuellt (siehe LabelTemplatesService.renderLabelsPdf).
|
||
model LabelTemplate {
|
||
id String @id @default(cuid())
|
||
name String
|
||
// Array of LabelElement objects (text/placeholder/qrCode/image/line), see
|
||
// label-templates.service.ts. Drives both the in-browser canvas designer
|
||
// and the pdfkit print renderer from the same source of truth.
|
||
elements Json @default("[]")
|
||
widthMm Float @default(90)
|
||
heightMm Float @default(35)
|
||
// A4 sheet layout. columns/rows are nullable: null means "auto-fit as many
|
||
// as fit" (the original behaviour); a real label sheet product (e.g. a
|
||
// specific Avery SKU) publishes an exact columns/rows/margin/gap grid, so
|
||
// setting these lets a template's printed positions match the physical,
|
||
// pre-die-cut sheet instead of just whatever the naive fit computes.
|
||
columns Int?
|
||
rows Int?
|
||
marginTopMm Float @default(5)
|
||
marginLeftMm Float @default(5)
|
||
gapXMm Float @default(2)
|
||
gapYMm Float @default(2)
|
||
// Kleine Schnittmarken im Blattrand, eine an jedem Ende jeder
|
||
// Schnittlinie - zum Anlegen am Papierschneider. Aus bei vorgestanzten
|
||
// Etikettenbögen, die nicht geschnitten werden.
|
||
cropMarks Boolean @default(true)
|
||
isDefault Boolean @default(false)
|
||
active Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([active])
|
||
}
|
||
|
||
model BottlingBatch {
|
||
id String @id @default(cuid())
|
||
honeyBatchId String
|
||
honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id])
|
||
productId String?
|
||
product Product? @relation(fields: [productId], references: [id])
|
||
date DateTime
|
||
jarSize Float
|
||
jarCount Int
|
||
totalAmount Float
|
||
bestBeforeDate DateTime
|
||
lotNumber String @unique
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([honeyBatchId])
|
||
@@index([productId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Inventar (Abschnitt 4.22) – v1.x Modul 24
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum EquipmentCondition {
|
||
NEW
|
||
GOOD
|
||
WORN
|
||
DEFECTIVE
|
||
}
|
||
|
||
model Equipment {
|
||
id String @id @default(cuid())
|
||
inventoryNumber String @unique
|
||
name String
|
||
manufacturer String?
|
||
purchaseDate DateTime?
|
||
condition EquipmentCondition @default(GOOD)
|
||
location String?
|
||
maintenanceDate DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([condition])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Benachrichtigungssystem (Abschnitt 24) – v1.x Modul 26
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum NotificationPriority {
|
||
INFO
|
||
HINWEIS
|
||
WARNUNG
|
||
KRITISCH
|
||
}
|
||
|
||
model Notification {
|
||
id String @id @default(cuid())
|
||
userId String?
|
||
user User? @relation(fields: [userId], references: [id])
|
||
priority NotificationPriority
|
||
message String
|
||
relatedType String?
|
||
relatedId String?
|
||
read Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId, read])
|
||
@@index([priority])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Webhooks (Abschnitt 27) – v1.x Modul 28
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model WebhookSubscription {
|
||
id String @id @default(cuid())
|
||
url String
|
||
events String[]
|
||
secret String
|
||
active Boolean @default(true)
|
||
createdByUserId String?
|
||
createdByUser User? @relation(fields: [createdByUserId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([active])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Dokumente und Bilder (Abschnitt 4.28) – v1.x Modul 29
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum DocumentEntityType {
|
||
APIARY
|
||
COLONY
|
||
QUEEN
|
||
INSPECTION
|
||
HONEY_BATCH
|
||
}
|
||
|
||
model Document {
|
||
id String @id @default(cuid())
|
||
entityType DocumentEntityType
|
||
entityId String
|
||
fileName String
|
||
mimeType String
|
||
sizeBytes Int
|
||
storagePath String
|
||
uploadedByUserId String?
|
||
uploadedByUser User? @relation(fields: [uploadedByUserId], references: [id])
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([entityType, entityId])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Automatisierungen (Abschnitt 26) – v1.x Modul 30
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Fest hinterlegte Trigger (Code, nicht nutzerdefiniert) – die Tabelle erlaubt
|
||
// nur, jeden Trigger zu aktivieren/deaktivieren und seine Schwellenwerte
|
||
// (config) sowie den Aufgabentext (actionTemplate) anzupassen.
|
||
enum AutomationTriggerType {
|
||
QUEENLESS_NO_EGGS
|
||
VARROA_THRESHOLD
|
||
FEED_STATUS_CRITICAL
|
||
SWARM_MOOD_HIGH
|
||
COLONY_HEALTH_CRITICAL
|
||
STOCK_ITEM_LOW
|
||
HONEY_BATCH_NUMBER_FORMAT
|
||
BOTTLING_LOT_NUMBER_FORMAT
|
||
TREATMENT_FOLLOW_UP
|
||
FEEDING_FOLLOW_UP
|
||
}
|
||
|
||
model AutomationRule {
|
||
id String @id @default(cuid())
|
||
triggerType AutomationTriggerType @unique
|
||
active Boolean @default(true)
|
||
config Json?
|
||
actionTemplate String?
|
||
// Running counter for the number-format rules (HONEY_BATCH_NUMBER_FORMAT /
|
||
// BOTTLING_LOT_NUMBER_FORMAT) - unused by the threshold-style rules. Kept as
|
||
// a dedicated column (not inside `config`) so it can be incremented with a
|
||
// single atomic `UPDATE ... SET sequenceValue = sequenceValue + 1`,
|
||
// race-safe under concurrent batch/bottling creation.
|
||
sequenceValue Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Website-Einstellungen – Social-Media-Links auf der öffentlichen Herkunftsseite
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Singleton row (fixed id) rather than a key/value table: exactly one set of
|
||
// site-wide social links exists, and a fixed-id upsert is simpler than
|
||
// enforcing "at most one row" any other way.
|
||
model SiteSettings {
|
||
id String @id @default("singleton")
|
||
youtubeUrl String?
|
||
instagramUrl String?
|
||
tiktokUrl String?
|
||
websiteUrl String?
|
||
// Name and address of the bee keeper, printed on the Bestandsbuch. Never
|
||
// part of any public page.
|
||
holderName String?
|
||
holderAddress String?
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Backup-Einstellungen – GUI-Override für den automatischen Backup-Zeitplan.
|
||
// Jede der drei Umgebungsvariablen (BACKUP_STORAGE_PATH/_CRON_SCHEDULE/
|
||
// _RETENTION_COUNT) hat Vorrang vor dem hier gespeicherten Wert - diese Zeile
|
||
// greift nur pro Feld, wenn die jeweilige Env-Variable nicht gesetzt ist.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
model BackupSettings {
|
||
id String @id @default("singleton")
|
||
storagePath String?
|
||
cronSchedule String?
|
||
retentionCount Int?
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Kalender-Synchronisation – einseitige Übertragung (ApiCore → M365) von
|
||
// Kalendereinträgen in den Kalender einer Microsoft-365-Gruppe via Graph API.
|
||
// Zugangsdaten (Tenant/Client-ID/-Secret) sind bewusst NICHT Teil dieses
|
||
// Modells - wie bei OIDC_CLIENT_SECRET nur über Env-Variablen (Secret), nie
|
||
// über die GUI editierbar. groupId ist keine Zugangsdaten-Information und
|
||
// daher hier GUI-konfigurierbar.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum CalendarSyncSourceType {
|
||
TASK
|
||
INSPECTION
|
||
TREATMENT
|
||
FEEDING
|
||
HARVEST
|
||
QUEEN_ASSIGNMENT
|
||
}
|
||
|
||
enum CalendarSyncStatus {
|
||
PENDING
|
||
SYNCED
|
||
FAILED
|
||
SKIPPED
|
||
}
|
||
|
||
model CalendarSyncSettings {
|
||
id String @id @default("singleton")
|
||
enabled Boolean @default(false)
|
||
groupId String?
|
||
syncTask Boolean @default(true)
|
||
syncInspection Boolean @default(true)
|
||
syncTreatment Boolean @default(true)
|
||
syncFeeding Boolean @default(true)
|
||
syncHarvest Boolean @default(true)
|
||
syncQueenAssignment Boolean @default(true)
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// Outbox statt synchronem Push direkt im Request-Pfad (anders als das
|
||
// Webhooks-Modul): Graph-API-Aufrufe sind zu langsam/fehleranfällig
|
||
// (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/
|
||
// Aufgaben-Erstellung darauf warten zu lassen. Ein periodischer Worker
|
||
// verarbeitet PENDING-Zeilen und wiederholt fehlgeschlagene Versuche.
|
||
// @@unique auf [sourceType, sourceId] macht enqueue() ein Upsert: ein
|
||
// erneutes Enqueue (z. B. nach einer Aufgaben-Änderung) aktualisiert das
|
||
// bestehende Graph-Event über die gespeicherte graphEventId, statt ein
|
||
// Duplikat anzulegen.
|
||
// ---------------------------------------------------------------------------
|
||
// Idempotenz – schützt Anlege-Anfragen vor doppelter Ausführung, wenn ein
|
||
// Client dieselbe Anfrage wiederholt (Offline-Warteschlange nach verlorener
|
||
// Antwort, Netzabbruch, erneutes Absenden). Der Client schickt pro fachlichem
|
||
// Vorgang einen eindeutigen `Idempotency-Key`; eine Wiederholung bekommt die
|
||
// gespeicherte erste Antwort statt eines zweiten Datensatzes. Siehe
|
||
// src/common/idempotency/idempotency.interceptor.ts.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
enum IdempotencyStatus {
|
||
PENDING
|
||
COMPLETED
|
||
}
|
||
|
||
model IdempotencyRecord {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
key String
|
||
// "POST /api/v1/inspections" - a key reused for a different endpoint is
|
||
// rejected instead of replaying an unrelated response.
|
||
scope String
|
||
status IdempotencyStatus @default(PENDING)
|
||
statusCode Int?
|
||
responseBody Json?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([userId, key])
|
||
@@index([createdAt])
|
||
}
|
||
|
||
model CalendarSyncOutboxEntry {
|
||
id String @id @default(cuid())
|
||
sourceType CalendarSyncSourceType
|
||
sourceId String
|
||
status CalendarSyncStatus @default(PENDING)
|
||
graphEventId String?
|
||
attempts Int @default(0)
|
||
lastError String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([sourceType, sourceId])
|
||
@@index([status])
|
||
}
|