Benutzer koennen jetzt selbst Zwei-Faktor-Authentifizierung einrichten
und ihr Profil verwalten (Name, E-Mail, Passwort, Profilbild).
Backend:
- Neues MfaModule (src/auth/mfa/): AES-256-GCM-verschluesselte
TOTP-Secrets (otplib), QR-Code-Erzeugung, kurzlebige Setup-/
Challenge-JWTs fuer den 2-Schritt-Login-Flow
- Neues ProfileModule (src/profile/): PATCH /profile,
POST /profile/password, POST/DELETE/GET /profile/avatar,
POST /profile/mfa/enroll(/confirm), POST /profile/mfa/disable -
alle self-scoped ueber actor.id, ohne @RequirePermission (siehe
ALLOWLIST in authorization-coverage.e2e-spec.ts)
- AuthService.login() zweistufig: bei aktivem MFA erst
{ mfaRequired, mfaToken } statt Tokens, neuer Endpunkt
POST /auth/mfa/verify schliesst den Flow ab
- Admin-Recovery: POST /users/:id/mfa/reset (users:update) fuer
verlorene MFA-Geraete, da es keinen E-Mail-Reset-Flow gibt
Frontend:
- Login-Seite mit zweitem Schritt fuer den TOTP-Code
- Neue Profilseite (/profile, ausserhalb des admin-only
Settings-Bereichs): Profilbild, Name/E-Mail, Passwort, MFA-Ein-/
Ausrichtung mit QR-Code-Dialog
- Header zeigt jetzt Avatar mit Dropdown (Profil/Abmelden) statt
nur E-Mail-Text
- Admin-Users-Tabelle zeigt MFA-Status + Reset-Button
Haertung nach Security-Review (Backend/Frontend-Diff, kein CRITICAL,
mehrere MEDIUM behoben):
- Access-Token wird bei Passwortwechsel sofort ungueltig
(User.passwordChangedAt + JWT-iat-Vergleich in
PermissionResolverService.isTokenValid, vorher isUserActive) statt
erst nach bis zu 15 Minuten
- E-Mail-Aenderung verlangt jetzt aktuelles Passwort
- POST /profile/password und /profile/mfa/disable haben jetzt das
gleiche strikte Throttling wie Login (SENSITIVE_ACTION_THROTTLE)
- Kein stiller leerer Verschluesselungs-Key-Fallback mehr (wirft
jetzt explizit)
Bewusst zurueckgestellt (dokumentiert, kein aktiver Exploit-Pfad):
dedizierter Verschluesselungs-Key fuer MFA-Secrets statt
JWT_ACCESS_SECRET-Ableitung, dedizierte users:mfa-reset-Permission
statt users:update, separate JWT-Secrets pro Token-Zweck.
Verifiziert: tsc/eslint clean, 297/297 Backend- + 31/31
Frontend-Unit-Tests, Build clean auf beiden Projekten. e2e weiterhin
durch bekanntes WSL2/Postgres-Problem blockiert. Unabhaengiges
Security- und Code-Quality-Review durchgefuehrt (0 CRITICAL, 0 HIGH).
986 lines
28 KiB
Plaintext
986 lines
28 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[]
|
||
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[]
|
||
|
||
@@index([roleId])
|
||
}
|
||
|
||
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[]
|
||
|
||
@@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
|
||
}
|
||
|
||
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 String?
|
||
feedStatus String?
|
||
healthStatus String?
|
||
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[]
|
||
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])
|
||
createdAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
|
||
@@index([status, dueDate])
|
||
@@index([relatedType, relatedId])
|
||
}
|
||
|
||
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])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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?
|
||
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[]
|
||
|
||
@@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")
|
||
|
||
@@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])
|
||
}
|
||
|
||
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])
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
model AutomationRule {
|
||
id String @id @default(cuid())
|
||
triggerType AutomationTriggerType @unique
|
||
active Boolean @default(true)
|
||
config Json?
|
||
actionTemplate String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|