Files
kita-planer/prisma/schema.prisma
T

1187 lines
35 KiB
Plaintext
Raw 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 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")
}