Files
2026-05-08 14:32:14 +02:00

669 lines
19 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =====================================================================
// 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 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")
}