1187 lines
35 KiB
Plaintext
1187 lines
35 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 RsvpMode {
|
||
CHILD
|
||
FAMILY
|
||
}
|
||
|
||
enum AnnouncementAudience {
|
||
ELTERN
|
||
ERZIEHER
|
||
ALLE
|
||
}
|
||
|
||
enum EventParticipationStatus {
|
||
ATTENDING
|
||
NOT_ATTENDING
|
||
}
|
||
|
||
enum AuditTargetType {
|
||
KITA
|
||
USER
|
||
}
|
||
|
||
enum NotdienstPlanStatus {
|
||
DRAFT
|
||
PUBLISHED
|
||
}
|
||
|
||
enum NotdienstAlertStatus {
|
||
PENDING
|
||
CONFIRMED
|
||
CANCELLED
|
||
}
|
||
|
||
enum NotdienstAlarmStatus {
|
||
PENDING
|
||
CONFIRMED
|
||
DECLINED
|
||
LATE_CONFIRMATION
|
||
REVIEW_REQUIRED
|
||
CANCELLED
|
||
}
|
||
|
||
enum NotdienstSwapStatus {
|
||
PENDING
|
||
ACCEPTED
|
||
DECLINED
|
||
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[]
|
||
eventParticipations EventParticipation[]
|
||
notdienstPlans NotdienstPlan[]
|
||
notdienstAvailabilities NotdienstAvailability[]
|
||
notdienstAssignments NotdienstAssignment[]
|
||
notdienstAlerts NotdienstAlert[]
|
||
notdienstAlarms NotdienstAlarm[]
|
||
notdienstSwapRequests NotdienstSwapRequest[]
|
||
staffProfiles StaffProfile[]
|
||
staffHolidays StaffHoliday[]
|
||
familyEventParticipations FamilyEventParticipation[]
|
||
|
||
@@map("kitas")
|
||
}
|
||
|
||
// =====================================================================
|
||
// FAMILIES / HAUSHALTE
|
||
// =====================================================================
|
||
|
||
model Family {
|
||
id String @id @default(cuid())
|
||
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
name String
|
||
notdienstAlarms NotdienstAlarm[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
users User[]
|
||
children Child[]
|
||
dutyAssignments DutyAssignment[]
|
||
notdienstSwapRequestsStarted NotdienstSwapRequest[] @relation("NotdienstSwapRequesterFamily")
|
||
familyEventParticipations FamilyEventParticipation[]
|
||
|
||
@@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?
|
||
staffDecisionEmails Boolean @default(true)
|
||
|
||
// 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")
|
||
notdienstSwapRequestsStarted NotdienstSwapRequest[] @relation("NotdienstSwapRequester")
|
||
notdienstSwapRequestsResponded NotdienstSwapRequest[] @relation("NotdienstSwapResponder")
|
||
announcementsAuthored Announcement[] @relation("AnnouncementAuthor")
|
||
announcementReads AnnouncementRead[]
|
||
|
||
termineCreated Termin[] @relation("TerminCreator")
|
||
termineApproved Termin[] @relation("TerminApprover")
|
||
|
||
mitbringselItems MitbringselItem[]
|
||
invitationsCreated Invitation[] @relation("InvitationCreator")
|
||
familyInvitation Invitation? @relation("InvitationRecipient")
|
||
auditLogsAuthored AuditLog[] @relation("AuditLogActor")
|
||
staffProfile StaffProfile?
|
||
staffNotifications StaffNotification[]
|
||
staffEmails StaffEmail[]
|
||
alarmRecipients NotdienstAlarmRecipient[]
|
||
alarmNotifications NotdienstAlarmNotification[]
|
||
alarmEmails NotdienstAlarmEmail[]
|
||
|
||
@@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[]
|
||
eventParticipations EventParticipation[]
|
||
|
||
@@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[]
|
||
staffProfile StaffProfile?
|
||
|
||
@@index([kitaId])
|
||
@@map("educators")
|
||
}
|
||
|
||
// Personalbereich: Tage als UTC-Datum, Zeiten als Minuten, Anspruch in halben Tagen.
|
||
enum StaffRequestStatus {
|
||
PENDING
|
||
APPROVED
|
||
REJECTED
|
||
CANCEL_REQUESTED
|
||
CANCELLED
|
||
}
|
||
|
||
enum StaffLeaveKind {
|
||
VACATION
|
||
TIME_OFF
|
||
SICKNESS
|
||
}
|
||
|
||
enum StaffWeekStatus {
|
||
DRAFT
|
||
SUBMITTED
|
||
APPROVED
|
||
}
|
||
|
||
model StaffProfile {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
educatorId String @unique
|
||
educator Educator @relation(fields: [educatorId], references: [id], onDelete: NoAction)
|
||
userId String? @unique
|
||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||
employmentStart DateTime @db.Date
|
||
employmentEnd DateTime? @db.Date
|
||
trackingStart DateTime @db.Date
|
||
openingMinutes Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
workPatterns StaffWorkPattern[]
|
||
dayOverrides StaffDayOverride[]
|
||
leaveAccounts StaffLeaveAccount[]
|
||
leaves StaffLeave[]
|
||
timeEntries StaffTimeEntry[]
|
||
weeks StaffWeek[]
|
||
adjustments StaffAdjustment[]
|
||
history StaffHistory[]
|
||
notifications StaffNotification[]
|
||
emails StaffEmail[]
|
||
|
||
@@index([kitaId])
|
||
@@map("staff_profiles")
|
||
}
|
||
|
||
// Persisted in the same transaction as the staff action; delivered after commit.
|
||
model StaffEmail {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
toAdmins Boolean @default(false)
|
||
subject String
|
||
message String
|
||
path String
|
||
inviteToken String?
|
||
createdAt DateTime @default(now())
|
||
sentAt DateTime?
|
||
skippedAt DateTime?
|
||
leaseUntil DateTime?
|
||
attempts Int @default(0)
|
||
failed Boolean @default(false)
|
||
|
||
@@index([profileId, sentAt, skippedAt])
|
||
@@map("staff_emails")
|
||
}
|
||
|
||
model StaffWorkPattern {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
effectiveFrom DateTime @db.Date
|
||
// Montag bis Sonntag; 0 bedeutet arbeitsfrei.
|
||
dailyMinutes Int[]
|
||
|
||
@@unique([profileId, effectiveFrom])
|
||
@@map("staff_work_patterns")
|
||
}
|
||
|
||
model StaffDayOverride {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
date DateTime @db.Date
|
||
minutes Int
|
||
reason String
|
||
|
||
@@unique([profileId, date])
|
||
@@map("staff_day_overrides")
|
||
}
|
||
|
||
model StaffHoliday {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
date DateTime @db.Date
|
||
name String
|
||
|
||
@@unique([kitaId, date])
|
||
@@map("staff_holidays")
|
||
}
|
||
|
||
model StaffLeaveAccount {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
year Int
|
||
entitlementUnits Int
|
||
carryUnits Int @default(0)
|
||
|
||
@@unique([profileId, year])
|
||
@@map("staff_leave_accounts")
|
||
}
|
||
|
||
model StaffLeave {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
kind StaffLeaveKind
|
||
status StaffRequestStatus @default(PENDING)
|
||
startDate DateTime @db.Date
|
||
endDate DateTime @db.Date
|
||
halfDay Boolean @default(false)
|
||
minutes Int?
|
||
note String?
|
||
decisionNote String?
|
||
requestedById String
|
||
reviewedById String?
|
||
reviewedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
days StaffLeaveDay[]
|
||
|
||
@@index([profileId, status, startDate])
|
||
@@map("staff_leaves")
|
||
}
|
||
|
||
model StaffLeaveDay {
|
||
id String @id @default(cuid())
|
||
leaveId String
|
||
leave StaffLeave @relation(fields: [leaveId], references: [id], onDelete: Cascade)
|
||
date DateTime @db.Date
|
||
vacationUnits Int @default(0)
|
||
creditMinutes Int
|
||
|
||
@@unique([leaveId, date])
|
||
@@map("staff_leave_days")
|
||
}
|
||
|
||
model StaffTimeEntry {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
date DateTime @db.Date
|
||
startMinute Int
|
||
endMinute Int
|
||
breakMinutes Int @default(0)
|
||
note String?
|
||
|
||
@@index([profileId, date])
|
||
@@map("staff_time_entries")
|
||
}
|
||
|
||
model StaffWeek {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
weekStart DateTime @db.Date
|
||
status StaffWeekStatus @default(DRAFT)
|
||
submittedAt DateTime?
|
||
reviewedAt DateTime?
|
||
reviewedById String?
|
||
decisionNote String?
|
||
// Snapshot von Soll-, Ist- und Abwesenheitsminuten bei Genehmigung.
|
||
dailySnapshot Json?
|
||
|
||
@@unique([profileId, weekStart])
|
||
@@map("staff_weeks")
|
||
}
|
||
|
||
model StaffAdjustment {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
date DateTime @db.Date
|
||
minutes Int
|
||
reason String
|
||
actorId String
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([profileId, date])
|
||
@@map("staff_adjustments")
|
||
}
|
||
|
||
model StaffHistory {
|
||
id String @id @default(cuid())
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
actorId String
|
||
action String
|
||
details String
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([profileId, createdAt])
|
||
@@map("staff_history")
|
||
}
|
||
|
||
model StaffNotification {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
profileId String
|
||
profile StaffProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||
message String
|
||
readAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId, readAt, createdAt])
|
||
@@map("staff_notifications")
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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
|
||
audience AnnouncementAudience @default(ALLE)
|
||
|
||
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)
|
||
|
||
// Nullable for older, unlinked invitations. Family invitations belong to a user.
|
||
userId String? @unique
|
||
user User? @relation("InvitationRecipient", fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
token String @unique
|
||
status InvitationStatus @default(PENDING)
|
||
expiresAt DateTime
|
||
acceptedAt DateTime?
|
||
sentAt DateTime?
|
||
lastAttemptAt DateTime?
|
||
leaseUntil DateTime?
|
||
attempts Int @default(0)
|
||
failed Boolean @default(false)
|
||
|
||
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)
|
||
|
||
/// Teilnahme-Abfrage für Ausflüge und besondere Events.
|
||
requiresRsvp Boolean @default(false)
|
||
rsvpMode RsvpMode @default(CHILD)
|
||
rsvpDeadline DateTime?
|
||
participantInfo String?
|
||
|
||
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[]
|
||
participations EventParticipation[]
|
||
familyParticipations FamilyEventParticipation[]
|
||
|
||
@@index([kitaId, startDate])
|
||
@@index([kitaId, status])
|
||
@@map("termine")
|
||
}
|
||
|
||
model EventParticipation {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
eventId String
|
||
event Termin @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||
|
||
childId String
|
||
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
|
||
|
||
status EventParticipationStatus
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([eventId, childId])
|
||
@@index([kitaId])
|
||
@@index([childId])
|
||
@@map("event_participations")
|
||
}
|
||
|
||
model FamilyEventParticipation {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
eventId String
|
||
event Termin @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||
familyId String
|
||
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
|
||
status EventParticipationStatus
|
||
adultCount Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([eventId, familyId])
|
||
@@index([kitaId])
|
||
@@index([familyId])
|
||
@@map("family_event_participations")
|
||
}
|
||
|
||
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[]
|
||
alarms NotdienstAlarm[]
|
||
swapRequestsAsTarget NotdienstSwapRequest[] @relation("NotdienstSwapTargetAssignment")
|
||
swapRequestsAsOffered NotdienstSwapRequest[] @relation("NotdienstSwapOfferedAssignment")
|
||
|
||
@@unique([planId, date])
|
||
@@index([kitaId, date])
|
||
@@map("notdienst_assignments")
|
||
}
|
||
|
||
/// Tauschanfrage zwischen Eltern fuer zwei bereits veroeffentlichte
|
||
/// zukuenftige Notdienst-Zuteilungen.
|
||
model NotdienstSwapRequest {
|
||
id String @id @default(cuid())
|
||
kitaId String
|
||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||
|
||
requesterUserId String
|
||
requesterUser User @relation("NotdienstSwapRequester", fields: [requesterUserId], references: [id], onDelete: Cascade)
|
||
|
||
requesterFamilyId String
|
||
requesterFamily Family @relation("NotdienstSwapRequesterFamily", fields: [requesterFamilyId], references: [id], onDelete: Cascade)
|
||
|
||
targetAssignmentId String
|
||
targetAssignment NotdienstAssignment @relation("NotdienstSwapTargetAssignment", fields: [targetAssignmentId], references: [id], onDelete: Cascade)
|
||
|
||
offeredAssignmentId String
|
||
offeredAssignment NotdienstAssignment @relation("NotdienstSwapOfferedAssignment", fields: [offeredAssignmentId], references: [id], onDelete: Cascade)
|
||
|
||
status NotdienstSwapStatus @default(PENDING)
|
||
message String?
|
||
|
||
respondedById String?
|
||
respondedBy User? @relation("NotdienstSwapResponder", fields: [respondedById], references: [id], onDelete: SetNull)
|
||
respondedAt DateTime?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([kitaId, status])
|
||
@@index([requesterFamilyId, status])
|
||
@@index([targetAssignmentId, status])
|
||
@@index([offeredAssignmentId, status])
|
||
@@map("notdienst_swap_requests")
|
||
}
|
||
|
||
/// Aktive Alarmierung — wird bei Krankmeldung einer Fachkraft
|
||
/// vom Koordinator ausgelöst. Bestätigung via Magic-Link (Token).
|
||
model NotdienstAlarm {
|
||
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: SetNull)
|
||
// Nullable unique reservation: cancelled or ended alarms release the assignment.
|
||
activeAssignmentId String? @unique
|
||
familyId String
|
||
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
|
||
familyName String
|
||
childName String
|
||
date DateTime @db.Date
|
||
wholeDay Boolean @default(true)
|
||
startMinute Int?
|
||
endMinute Int?
|
||
expiresAt DateTime
|
||
deadline DateTime
|
||
status NotdienstAlarmStatus @default(PENDING)
|
||
triggeredById String?
|
||
triggeredAt DateTime @default(now())
|
||
respondedById String?
|
||
respondedAt DateTime?
|
||
responseSource String?
|
||
escalatedAt DateTime?
|
||
educatorId String?
|
||
notes String?
|
||
cancelledAt DateTime?
|
||
cancelReason String?
|
||
recipients NotdienstAlarmRecipient[]
|
||
notifications NotdienstAlarmNotification[]
|
||
emails NotdienstAlarmEmail[]
|
||
history NotdienstAlarmHistory[]
|
||
legacyAlerts NotdienstAlert[]
|
||
|
||
@@index([kitaId, date])
|
||
@@index([status, deadline])
|
||
@@map("notdienst_alarms")
|
||
}
|
||
|
||
model NotdienstAlarmRecipient {
|
||
id String @id @default(cuid())
|
||
alarmId String
|
||
alarm NotdienstAlarm @relation(fields: [alarmId], references: [id], onDelete: Cascade)
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
token String @unique
|
||
|
||
@@unique([alarmId, userId])
|
||
@@map("notdienst_alarm_recipients")
|
||
}
|
||
|
||
model NotdienstAlarmNotification {
|
||
id String @id @default(cuid())
|
||
alarmId String
|
||
alarm NotdienstAlarm @relation(fields: [alarmId], references: [id], onDelete: Cascade)
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
eventKey String
|
||
message String
|
||
createdAt DateTime @default(now())
|
||
readAt DateTime?
|
||
|
||
@@unique([alarmId, userId, eventKey])
|
||
@@index([userId, readAt])
|
||
@@map("notdienst_alarm_notifications")
|
||
}
|
||
|
||
model NotdienstAlarmEmail {
|
||
id String @id @default(cuid())
|
||
alarmId String
|
||
alarm NotdienstAlarm @relation(fields: [alarmId], references: [id], onDelete: Cascade)
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
eventKey String
|
||
kind String
|
||
subject String
|
||
message String
|
||
path String
|
||
createdAt DateTime @default(now())
|
||
sentAt DateTime?
|
||
skippedAt DateTime?
|
||
leaseUntil DateTime?
|
||
failed Boolean @default(false)
|
||
attempts Int @default(0)
|
||
|
||
@@unique([alarmId, userId, eventKey])
|
||
@@index([sentAt, skippedAt, createdAt])
|
||
@@map("notdienst_alarm_emails")
|
||
}
|
||
|
||
model NotdienstAlarmHistory {
|
||
id String @id @default(cuid())
|
||
alarmId String
|
||
alarm NotdienstAlarm @relation(fields: [alarmId], references: [id], onDelete: Cascade)
|
||
actorId String?
|
||
action String
|
||
details String
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([alarmId, createdAt])
|
||
@@map("notdienst_alarm_history")
|
||
}
|
||
|
||
model NotdienstAlert {
|
||
id String @id @default(cuid())
|
||
alarmId String?
|
||
alarm NotdienstAlarm? @relation(fields: [alarmId], references: [id], onDelete: SetNull)
|
||
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")
|
||
}
|
||
|
||
// =====================================================================
|
||
// AUDIT LOGS (Systemweite Superadmin-Aktionen)
|
||
// =====================================================================
|
||
|
||
/// Persistente Spur fuer Superadmin-Support-Aktionen.
|
||
/// Zielobjekte werden bewusst nur als Snapshot gespeichert, damit Logs auch
|
||
/// nach einer Kita- oder Benutzerloeschung erhalten bleiben.
|
||
model AuditLog {
|
||
id String @id @default(cuid())
|
||
|
||
actorUserId String?
|
||
actorUser User? @relation("AuditLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
||
actorEmail String
|
||
|
||
action String
|
||
targetType AuditTargetType
|
||
targetId String
|
||
targetLabel String
|
||
targetKitaId String?
|
||
|
||
metadata Json?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([actorUserId])
|
||
@@index([targetType, targetId])
|
||
@@index([targetKitaId])
|
||
@@index([createdAt])
|
||
@@map("audit_logs")
|
||
}
|
||
|
||
// =====================================================================
|
||
// 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")
|
||
}
|