Add non-destructive dev seed on startup
This commit is contained in:
@@ -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 1–12.
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user