Add non-destructive dev seed on startup

This commit is contained in:
t.indorf
2026-05-06 22:31:07 +02:00
parent 9f452fccac
commit b686e714ff
77 changed files with 10862 additions and 87 deletions
+512
View File
@@ -0,0 +1,512 @@
// =====================================================================
// Kita-Planer · Prisma Schema
// ---------------------------------------------------------------------
// Multi-Tenant-SaaS für Elternvereine. Tenant = `Kita`.
//
// Sicherheits-Leitplanken (siehe planung.md §5):
// • Jedes mandantengebundene Modell trägt `kitaId` und ist über
// `Kita` per `onDelete: Cascade` verknüpft → DSGVO-konformes
// "Recht auf Vergessenwerden".
// • Auf Applikations-/Server-Action-Ebene MUSS jeder Query mit
// `where: { kitaId: session.user.kitaId }` versehen werden.
// • Keine Soft-Deletes auf personenbezogene Daten.
// =====================================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =====================================================================
// ENUMS
// =====================================================================
/// Rollen-basierte Zugriffskontrolle (RBAC).
/// SUPERADMIN existiert systemweit (kitaId optional);
/// ADMIN, KOORDINATOR und ELTERN sind immer einer Kita zugeordnet.
enum UserRole {
SUPERADMIN
ADMIN
KOORDINATOR
ELTERN
}
enum InvitationStatus {
PENDING
ACCEPTED
EXPIRED
REVOKED
}
enum TerminType {
KITA_FEST
SCHLIESSTAG
TEAMTAG
MITMACH_TAG
ELTERNABEND
MITGLIEDERVERSAMMLUNG
ELTERNCAFE
GEBURTSTAG_INTERN
GEBURTSTAG_EXTERN
SONSTIGES
}
enum TerminStatus {
PENDING
CONFIRMED
REJECTED
CANCELLED
}
enum NotdienstPlanStatus {
DRAFT
PUBLISHED
}
enum NotdienstAlertStatus {
PENDING
CONFIRMED
CANCELLED
}
// =====================================================================
// TENANT
// =====================================================================
model Kita {
id String @id @default(cuid())
name String
slug String @unique
// Modul-Aktivierung (Einrichtungs-Wizard, Modul 0)
notdienstModuleEnabled Boolean @default(true)
terminModuleEnabled Boolean @default(true)
adressbuchModuleEnabled Boolean @default(true)
// Mandantenspezifische Regeln
notdienstMinPerChildPerMonth Int @default(2)
notdienstReminderDaysBefore Int @default(7)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations (Cascade auf alle Tenant-Daten)
users User[]
children Child[]
educators Educator[]
parentDuties ParentDuty[]
parentDutyAssignments ParentDutyAssignment[]
invitations Invitation[]
termine Termin[]
mitbringselItems MitbringselItem[]
notdienstPlans NotdienstPlan[]
notdienstAvailabilities NotdienstAvailability[]
notdienstAssignments NotdienstAssignment[]
notdienstAlerts NotdienstAlert[]
childParents ChildParent[]
@@map("kitas")
}
// =====================================================================
// USERS
// =====================================================================
model User {
id String @id @default(cuid())
/// Mandantenzuordnung. Nullable nur für `SUPERADMIN`.
/// Bei Löschung der Kita werden alle zugeordneten User kaskadiert.
kitaId String?
kita Kita? @relation(fields: [kitaId], references: [id], onDelete: Cascade)
email String @unique
passwordHash String
firstName String
lastName String
role UserRole @default(ELTERN)
// Adressbuch / Kontaktdaten (nur sichtbar bei directoryOptIn)
phone String?
street String?
postalCode String?
city String?
// ---- DSGVO Consent Logging (planung.md §5.2) ----
/// Zeitstempel der Annahme der Datenschutzerklärung.
/// Ohne diesen Wert blockiert die App den Zugriff (Redirect → Onboarding).
privacyPolicyAcceptedAt DateTime?
/// Versionsstring der akzeptierten Datenschutzerklärung (z.B. "2025-04-01").
privacyPolicyVersion String?
/// Zeitstempel des Opt-Ins für das interne Kita-Adressbuch (Modul 3).
directoryOptInAt DateTime?
// Auth-Bookkeeping
emailVerifiedAt DateTime?
lastLoginAt DateTime?
failedLoginAttempts Int @default(0)
lockedUntil DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
childLinks ChildParent[]
dutyAssignments ParentDutyAssignment[]
notdienstAvailabilities NotdienstAvailability[]
notdienstAlertsAssigned NotdienstAlert[] @relation("NotdienstAlertParent")
notdienstAlertsTriggered NotdienstAlert[] @relation("NotdienstAlertTrigger")
notdienstPlansCreated NotdienstPlan[] @relation("NotdienstPlanCreator")
termineCreated Termin[] @relation("TerminCreator")
termineApproved Termin[] @relation("TerminApprover")
mitbringselItems MitbringselItem[]
invitationsCreated Invitation[] @relation("InvitationCreator")
@@index([kitaId])
@@index([kitaId, role])
@@map("users")
}
// =====================================================================
// CHILDREN
// =====================================================================
model Child {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
firstName String
lastName String
dateOfBirth DateTime?
notes String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parentLinks ChildParent[]
notdienstAvailabilities NotdienstAvailability[]
notdienstAssignments NotdienstAssignment[]
@@index([kitaId])
@@map("children")
}
/// Verknüpfung Kind ↔ Elternteil (m:n).
/// Kaskadiert über beide Seiten, damit das Löschen eines Users
/// oder Kindes keine Datenleichen hinterlässt.
model ChildParent {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([childId, userId])
@@index([kitaId])
@@index([userId])
@@map("child_parents")
}
// =====================================================================
// EDUCATORS (ErzieherInnen — reine Stammdaten, keine Logins, Modul 4)
// =====================================================================
model Educator {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
firstName String
lastName String
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notdienstAlerts NotdienstAlert[]
@@index([kitaId])
@@map("educators")
}
// =====================================================================
// PARENT DUTIES (Feste Ämter / Elterndienste, Modul 3)
// =====================================================================
model ParentDuty {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
name String
description String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments ParentDutyAssignment[]
@@unique([kitaId, name])
@@index([kitaId])
@@map("parent_duties")
}
model ParentDutyAssignment {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
dutyId String
duty ParentDuty @relation(fields: [dutyId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([dutyId, userId])
@@index([kitaId])
@@index([userId])
@@map("parent_duty_assignments")
}
// =====================================================================
// INVITATIONS (Invite-Only Onboarding, Modul 3)
// =====================================================================
model Invitation {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
email String
role UserRole @default(ELTERN)
token String @unique
status InvitationStatus @default(PENDING)
expiresAt DateTime
acceptedAt DateTime?
invitedById String?
invitedBy User? @relation("InvitationCreator", fields: [invitedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([kitaId, email])
@@index([kitaId])
@@map("invitations")
}
// =====================================================================
// TERMINE (Kalender, Modul 2)
// =====================================================================
model Termin {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
title String
description String?
type TerminType
status TerminStatus @default(PENDING)
startDate DateTime
endDate DateTime
allDay Boolean @default(false)
/// Mitbringliste pro Termin aktivierbar (Modul 2, "Flexible Mitbring-Listen")
mitbringselListEnabled Boolean @default(false)
createdById String?
createdBy User? @relation("TerminCreator", fields: [createdById], references: [id], onDelete: SetNull)
approvedById String?
approvedBy User? @relation("TerminApprover", fields: [approvedById], references: [id], onDelete: SetNull)
approvedAt DateTime?
rejectionReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
mitbringselItems MitbringselItem[]
@@index([kitaId, startDate])
@@index([kitaId, status])
@@map("termine")
}
model MitbringselItem {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
terminId String
termin Termin @relation(fields: [terminId], references: [id], onDelete: Cascade)
/// Cascade: Beim Löschen eines Accounts werden dessen Mitbringsel-Einträge
/// rückstandslos mitgelöscht (DSGVO "Recht auf Vergessenwerden").
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kitaId])
@@index([terminId])
@@map("mitbringsel_items")
}
// =====================================================================
// NOTDIENST (Modul 1 Kern-Feature)
// =====================================================================
/// Monatlicher Notdienst-Plan. Erst DRAFT (Koordinator bearbeitet),
/// nach "Veröffentlichung" PUBLISHED.
model NotdienstPlan {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
year Int
/// Monat 112.
month Int
status NotdienstPlanStatus @default(DRAFT)
createdById String?
createdBy User? @relation("NotdienstPlanCreator", fields: [createdById], references: [id], onDelete: SetNull)
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments NotdienstAssignment[]
@@unique([kitaId, year, month])
@@index([kitaId])
@@map("notdienst_plans")
}
/// Verfügbarkeits-Eintrag eines Elternteils für ein Kind an einem Tag.
/// Pro (Kind, Datum) maximal ein Eintrag — Geschwister-Doppelbuchungen
/// werden über die App-Logik geblockt.
model NotdienstAvailability {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
/// Eintragender User (für Audit / Erinnerungs-Cronjob).
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
date DateTime @db.Date
createdAt DateTime @default(now())
@@unique([childId, date])
@@index([kitaId, date])
@@index([userId])
@@map("notdienst_availabilities")
}
/// Eingeteilter Notdienst-Slot — Ergebnis der Plan-Generierung
/// bzw. manueller Bearbeitung durch den Koordinator.
model NotdienstAssignment {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
planId String
plan NotdienstPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
date DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
alerts NotdienstAlert[]
@@unique([planId, date])
@@index([kitaId, date])
@@map("notdienst_assignments")
}
/// Aktive Alarmierung — wird bei Krankmeldung einer Fachkraft
/// vom Koordinator ausgelöst. Bestätigung via Magic-Link (Token).
model NotdienstAlert {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
assignmentId String
assignment NotdienstAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
/// Eingeteiltes Elternteil (Empfänger des Alarms).
parentUserId String
parentUser User @relation("NotdienstAlertParent", fields: [parentUserId], references: [id], onDelete: Cascade)
/// Auslösender Koordinator.
triggeredById String?
triggeredBy User? @relation("NotdienstAlertTrigger", fields: [triggeredById], references: [id], onDelete: SetNull)
/// Optional: kranke Fachkraft (Referenz für Reporting).
educatorId String?
educator Educator? @relation(fields: [educatorId], references: [id], onDelete: SetNull)
status NotdienstAlertStatus @default(PENDING)
/// Einmal-Token für den Bestätigungslink in der Alarm-Mail.
confirmationToken String @unique
triggeredAt DateTime @default(now())
confirmedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kitaId])
@@index([assignmentId])
@@index([parentUserId])
@@map("notdienst_alerts")
}
// =====================================================================
// AUTH-HILFSTABELLE
// =====================================================================
/// Tokens für Passwort-Reset und E-Mail-Verifikation.
/// Kompatibel mit dem NextAuth-Schema, falls später Email-Provider aktiviert wird.
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}