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