// ===================================================================== // 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") }