669 lines
19 KiB
Plaintext
669 lines
19 KiB
Plaintext
// =====================================================================
|
||
// 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
|
||
ERZIEHER
|
||
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
|
||
}
|
||
|
||
enum DutyAssignmentStatus {
|
||
PLANNED
|
||
DONE
|
||
CANCELLED
|
||
}
|
||
|
||
enum AbsenceReason {
|
||
ILLNESS
|
||
VACATION
|
||
OTHER
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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[]
|
||
families Family[]
|
||
children Child[]
|
||
educators Educator[]
|
||
parentDuties ParentDuty[]
|
||
parentDutyAssignments ParentDutyAssignment[]
|
||
dutyTypes DutyType[]
|
||
dutyAssignments DutyAssignment[]
|
||
absences Absence[]
|
||
announcements Announcement[]
|
||
invitations Invitation[]
|
||
termine Termin[]
|
||
mitbringselItems MitbringselItem[]
|
||
notdienstPlans NotdienstPlan[]
|
||
notdienstAvailabilities NotdienstAvailability[]
|
||
notdienstAssignments NotdienstAssignment[]
|
||
notdienstAlerts NotdienstAlert[]
|
||
|
||
@@map("kitas")
|
||
}
|
||
|
||
// =====================================================================
|
||
// FAMILIES / HAUSHALTE
|
||
// =====================================================================
|
||
|
||
model Family {
|
||
id String @id @default(cuid())
|
||
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
name String
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
users User[]
|
||
children Child[]
|
||
dutyAssignments DutyAssignment[]
|
||
|
||
@@index([kitaId])
|
||
@@map("families")
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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)
|
||
|
||
/// Optional, weil Admins/Superadmins keinem Haushalt angehören müssen.
|
||
/// Eltern-User werden beim Löschen ihrer Familie kaskadiert entfernt.
|
||
familyId String?
|
||
family Family? @relation(fields: [familyId], 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
|
||
dutyAssignments ParentDutyAssignment[]
|
||
|
||
notdienstAvailabilities NotdienstAvailability[]
|
||
notdienstAlertsAssigned NotdienstAlert[] @relation("NotdienstAlertParent")
|
||
notdienstAlertsTriggered NotdienstAlert[] @relation("NotdienstAlertTrigger")
|
||
notdienstPlansCreated NotdienstPlan[] @relation("NotdienstPlanCreator")
|
||
announcementsAuthored Announcement[] @relation("AnnouncementAuthor")
|
||
announcementReads AnnouncementRead[]
|
||
|
||
termineCreated Termin[] @relation("TerminCreator")
|
||
termineApproved Termin[] @relation("TerminApprover")
|
||
|
||
mitbringselItems MitbringselItem[]
|
||
invitationsCreated Invitation[] @relation("InvitationCreator")
|
||
|
||
@@index([kitaId])
|
||
@@index([familyId])
|
||
@@index([kitaId, role])
|
||
@@map("users")
|
||
}
|
||
|
||
// =====================================================================
|
||
// CHILDREN
|
||
// =====================================================================
|
||
|
||
model Child {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
familyId String
|
||
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
|
||
|
||
firstName String
|
||
lastName String
|
||
dateOfBirth DateTime?
|
||
notes String?
|
||
|
||
active Boolean @default(true)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
notdienstAvailabilities NotdienstAvailability[]
|
||
notdienstAssignments NotdienstAssignment[]
|
||
absences Absence[]
|
||
|
||
@@index([kitaId])
|
||
@@index([familyId])
|
||
@@map("children")
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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")
|
||
}
|
||
|
||
// =====================================================================
|
||
// DUTY PLANNING (Top-Down-Dienstplan fuer Haushalte)
|
||
// =====================================================================
|
||
|
||
model DutyType {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
name String
|
||
description String?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
assignments DutyAssignment[]
|
||
|
||
@@unique([kitaId, name])
|
||
@@index([kitaId])
|
||
@@map("duty_types")
|
||
}
|
||
|
||
model DutyAssignment {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
familyId String
|
||
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
|
||
|
||
dutyTypeId String
|
||
dutyType DutyType @relation(fields: [dutyTypeId], references: [id], onDelete: Cascade)
|
||
|
||
startDate DateTime @db.Date
|
||
endDate DateTime @db.Date
|
||
status DutyAssignmentStatus @default(PLANNED)
|
||
|
||
reminderSentAt DateTime?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([kitaId, dutyTypeId, startDate])
|
||
@@index([kitaId, startDate])
|
||
@@index([familyId, startDate])
|
||
@@map("duty_assignments")
|
||
}
|
||
|
||
// =====================================================================
|
||
// ABSENCES (Abwesenheits- und Krankmeldungen)
|
||
// =====================================================================
|
||
|
||
model Absence {
|
||
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)
|
||
|
||
startDate DateTime @db.Date
|
||
endDate DateTime @db.Date
|
||
reason AbsenceReason
|
||
note String?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([kitaId, startDate, endDate])
|
||
@@index([childId, startDate])
|
||
@@map("absences")
|
||
}
|
||
|
||
// =====================================================================
|
||
// ANNOUNCEMENTS (Digitales Schwarzes Brett)
|
||
// =====================================================================
|
||
|
||
model Announcement {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
title String
|
||
content String
|
||
|
||
authorId String
|
||
author User @relation("AnnouncementAuthor", fields: [authorId], references: [id], onDelete: Cascade)
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
attachments Attachment[]
|
||
reads AnnouncementRead[]
|
||
|
||
@@index([kitaId, createdAt])
|
||
@@index([authorId])
|
||
@@map("announcements")
|
||
}
|
||
|
||
model Attachment {
|
||
id String @id @default(cuid())
|
||
announcementId String
|
||
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
||
|
||
fileName String
|
||
fileUrl String
|
||
fileType String
|
||
|
||
@@index([announcementId])
|
||
@@map("attachments")
|
||
}
|
||
|
||
model AnnouncementRead {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
announcementId String
|
||
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
||
|
||
readAt DateTime @default(now())
|
||
|
||
@@unique([userId, announcementId])
|
||
@@index([announcementId])
|
||
@@map("announcement_reads")
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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")
|
||
}
|