Merge current features with security hardening and automatic safe initialization
This commit is contained in:
+418
-29
@@ -63,6 +63,17 @@ enum TerminStatus {
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum RsvpMode {
|
||||
CHILD
|
||||
FAMILY
|
||||
}
|
||||
|
||||
enum AnnouncementAudience {
|
||||
ELTERN
|
||||
ERZIEHER
|
||||
ALLE
|
||||
}
|
||||
|
||||
enum EventParticipationStatus {
|
||||
ATTENDING
|
||||
NOT_ATTENDING
|
||||
@@ -84,6 +95,15 @@ enum NotdienstAlertStatus {
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum NotdienstAlarmStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
DECLINED
|
||||
LATE_CONFIRMATION
|
||||
REVIEW_REQUIRED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum NotdienstSwapStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
@@ -143,7 +163,11 @@ model Kita {
|
||||
notdienstAvailabilities NotdienstAvailability[]
|
||||
notdienstAssignments NotdienstAssignment[]
|
||||
notdienstAlerts NotdienstAlert[]
|
||||
notdienstAlarms NotdienstAlarm[]
|
||||
notdienstSwapRequests NotdienstSwapRequest[]
|
||||
staffProfiles StaffProfile[]
|
||||
staffHolidays StaffHoliday[]
|
||||
familyEventParticipations FamilyEventParticipation[]
|
||||
|
||||
@@map("kitas")
|
||||
}
|
||||
@@ -158,15 +182,17 @@ model Family {
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
name String
|
||||
notdienstAlarms NotdienstAlarm[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users User[]
|
||||
children Child[]
|
||||
dutyAssignments DutyAssignment[]
|
||||
users User[]
|
||||
children Child[]
|
||||
dutyAssignments DutyAssignment[]
|
||||
notdienstSwapRequestsStarted NotdienstSwapRequest[] @relation("NotdienstSwapRequesterFamily")
|
||||
familyEventParticipations FamilyEventParticipation[]
|
||||
|
||||
@@index([kitaId])
|
||||
@@map("families")
|
||||
@@ -212,6 +238,7 @@ model User {
|
||||
privacyPolicyVersion String?
|
||||
/// Zeitstempel des Opt-Ins für das interne Kita-Adressbuch (Modul 3).
|
||||
directoryOptInAt DateTime?
|
||||
staffDecisionEmails Boolean @default(true)
|
||||
|
||||
// Auth-Bookkeeping
|
||||
emailVerifiedAt DateTime?
|
||||
@@ -227,21 +254,27 @@ model User {
|
||||
// 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[]
|
||||
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")
|
||||
auditLogsAuthored AuditLog[] @relation("AuditLogActor")
|
||||
invitationsCreated Invitation[] @relation("InvitationCreator")
|
||||
auditLogsAuthored AuditLog[] @relation("AuditLogActor")
|
||||
staffProfile StaffProfile?
|
||||
staffNotifications StaffNotification[]
|
||||
staffEmails StaffEmail[]
|
||||
alarmRecipients NotdienstAlarmRecipient[]
|
||||
alarmNotifications NotdienstAlarmNotification[]
|
||||
alarmEmails NotdienstAlarmEmail[]
|
||||
|
||||
@@index([kitaId])
|
||||
@@index([familyId])
|
||||
@@ -300,12 +333,238 @@ model Educator {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
notdienstAlerts NotdienstAlert[]
|
||||
staffProfile StaffProfile?
|
||||
|
||||
@@index([kitaId])
|
||||
@@map("educators")
|
||||
@@unique([id, kitaId])
|
||||
}
|
||||
|
||||
// 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)
|
||||
// =====================================================================
|
||||
@@ -430,8 +689,9 @@ model Announcement {
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
title String
|
||||
content String
|
||||
title String
|
||||
content String
|
||||
audience AnnouncementAudience @default(ALLE)
|
||||
|
||||
authorId String
|
||||
author User @relation("AnnouncementAuthor", fields: [authorId], references: [id], onDelete: Cascade)
|
||||
@@ -528,6 +788,7 @@ model Termin {
|
||||
|
||||
/// Teilnahme-Abfrage für Ausflüge und besondere Events.
|
||||
requiresRsvp Boolean @default(false)
|
||||
rsvpMode RsvpMode @default(CHILD)
|
||||
rsvpDeadline DateTime?
|
||||
participantInfo String?
|
||||
|
||||
@@ -542,8 +803,9 @@ model Termin {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
mitbringselItems MitbringselItem[]
|
||||
participations EventParticipation[]
|
||||
mitbringselItems MitbringselItem[]
|
||||
participations EventParticipation[]
|
||||
familyParticipations FamilyEventParticipation[]
|
||||
|
||||
@@index([kitaId, startDate])
|
||||
@@index([kitaId, status])
|
||||
@@ -573,6 +835,25 @@ model EventParticipation {
|
||||
@@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
|
||||
@@ -671,7 +952,8 @@ model NotdienstAssignment {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
alerts NotdienstAlert[]
|
||||
alerts NotdienstAlert[]
|
||||
alarms NotdienstAlarm[]
|
||||
swapRequestsAsTarget NotdienstSwapRequest[] @relation("NotdienstSwapTargetAssignment")
|
||||
swapRequestsAsOffered NotdienstSwapRequest[] @relation("NotdienstSwapOfferedAssignment")
|
||||
|
||||
@@ -700,11 +982,11 @@ model NotdienstSwapRequest {
|
||||
offeredAssignmentId String
|
||||
offeredAssignment NotdienstAssignment @relation("NotdienstSwapOfferedAssignment", fields: [offeredAssignmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
status NotdienstSwapStatus @default(PENDING)
|
||||
status NotdienstSwapStatus @default(PENDING)
|
||||
message String?
|
||||
|
||||
respondedById String?
|
||||
respondedBy User? @relation("NotdienstSwapResponder", fields: [respondedById], references: [id], onDelete: SetNull)
|
||||
respondedBy User? @relation("NotdienstSwapResponder", fields: [respondedById], references: [id], onDelete: SetNull)
|
||||
respondedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
@@ -719,10 +1001,117 @@ model NotdienstSwapRequest {
|
||||
|
||||
/// 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
|
||||
sessionVersion Int @default(0)
|
||||
|
||||
@@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())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
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)
|
||||
@@ -771,13 +1160,13 @@ model AuditLog {
|
||||
actorUser User? @relation("AuditLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
||||
actorEmail String
|
||||
|
||||
action String
|
||||
targetType AuditTargetType
|
||||
targetId String
|
||||
targetLabel String
|
||||
action String
|
||||
targetType AuditTargetType
|
||||
targetId String
|
||||
targetLabel String
|
||||
targetKitaId String?
|
||||
|
||||
metadata Json?
|
||||
metadata Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([actorUserId])
|
||||
|
||||
Reference in New Issue
Block a user