Merge current features with security hardening and automatic safe initialization

This commit is contained in:
t.indorf
2026-10-09 23:29:19 +02:00
85 changed files with 13522 additions and 1326 deletions
@@ -0,0 +1,483 @@
BEGIN;
-- CreateEnum
CREATE TYPE "RsvpMode" AS ENUM ('CHILD', 'FAMILY');
-- CreateEnum
CREATE TYPE "AnnouncementAudience" AS ENUM ('ELTERN', 'ERZIEHER', 'ALLE');
-- CreateEnum
CREATE TYPE "NotdienstAlarmStatus" AS ENUM ('PENDING', 'CONFIRMED', 'DECLINED', 'LATE_CONFIRMATION', 'REVIEW_REQUIRED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "StaffRequestStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'CANCEL_REQUESTED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "StaffLeaveKind" AS ENUM ('VACATION', 'TIME_OFF', 'SICKNESS');
-- CreateEnum
CREATE TYPE "StaffWeekStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'APPROVED');
-- AlterTable
ALTER TABLE "users" ADD COLUMN "staffDecisionEmails" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "announcements" ADD COLUMN "audience" "AnnouncementAudience" NOT NULL DEFAULT 'ALLE';
-- AlterTable
ALTER TABLE "termine" ADD COLUMN "rsvpMode" "RsvpMode" NOT NULL DEFAULT 'CHILD';
-- AlterTable
ALTER TABLE "notdienst_alerts" ADD COLUMN "alarmId" TEXT;
-- CreateTable
CREATE TABLE "staff_profiles" (
"id" TEXT NOT NULL,
"kitaId" TEXT NOT NULL,
"educatorId" TEXT NOT NULL,
"userId" TEXT,
"employmentStart" DATE NOT NULL,
"employmentEnd" DATE,
"trackingStart" DATE NOT NULL,
"openingMinutes" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_emails" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"toAdmins" BOOLEAN NOT NULL DEFAULT false,
"subject" TEXT NOT NULL,
"message" TEXT NOT NULL,
"path" TEXT NOT NULL,
"inviteToken" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"sentAt" TIMESTAMP(3),
"skippedAt" TIMESTAMP(3),
"leaseUntil" TIMESTAMP(3),
"attempts" INTEGER NOT NULL DEFAULT 0,
"failed" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "staff_emails_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_work_patterns" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"effectiveFrom" DATE NOT NULL,
"dailyMinutes" INTEGER[],
CONSTRAINT "staff_work_patterns_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_day_overrides" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"date" DATE NOT NULL,
"minutes" INTEGER NOT NULL,
"reason" TEXT NOT NULL,
CONSTRAINT "staff_day_overrides_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_holidays" (
"id" TEXT NOT NULL,
"kitaId" TEXT NOT NULL,
"date" DATE NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "staff_holidays_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_leave_accounts" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"year" INTEGER NOT NULL,
"entitlementUnits" INTEGER NOT NULL,
"carryUnits" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "staff_leave_accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_leaves" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"kind" "StaffLeaveKind" NOT NULL,
"status" "StaffRequestStatus" NOT NULL DEFAULT 'PENDING',
"startDate" DATE NOT NULL,
"endDate" DATE NOT NULL,
"halfDay" BOOLEAN NOT NULL DEFAULT false,
"minutes" INTEGER,
"note" TEXT,
"decisionNote" TEXT,
"requestedById" TEXT NOT NULL,
"reviewedById" TEXT,
"reviewedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_leaves_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_leave_days" (
"id" TEXT NOT NULL,
"leaveId" TEXT NOT NULL,
"date" DATE NOT NULL,
"vacationUnits" INTEGER NOT NULL DEFAULT 0,
"creditMinutes" INTEGER NOT NULL,
CONSTRAINT "staff_leave_days_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_time_entries" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"date" DATE NOT NULL,
"startMinute" INTEGER NOT NULL,
"endMinute" INTEGER NOT NULL,
"breakMinutes" INTEGER NOT NULL DEFAULT 0,
"note" TEXT,
CONSTRAINT "staff_time_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_weeks" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"weekStart" DATE NOT NULL,
"status" "StaffWeekStatus" NOT NULL DEFAULT 'DRAFT',
"submittedAt" TIMESTAMP(3),
"reviewedAt" TIMESTAMP(3),
"reviewedById" TEXT,
"decisionNote" TEXT,
"dailySnapshot" JSONB,
CONSTRAINT "staff_weeks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_adjustments" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"date" DATE NOT NULL,
"minutes" INTEGER NOT NULL,
"reason" TEXT NOT NULL,
"actorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_adjustments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_history" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"actorId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"details" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_history_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "staff_notifications" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"message" TEXT NOT NULL,
"readAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "staff_notifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "family_event_participations" (
"id" TEXT NOT NULL,
"kitaId" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"familyId" TEXT NOT NULL,
"status" "EventParticipationStatus" NOT NULL,
"adultCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "family_event_participations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notdienst_alarms" (
"id" TEXT NOT NULL,
"kitaId" TEXT NOT NULL,
"assignmentId" TEXT,
"activeAssignmentId" TEXT,
"familyId" TEXT NOT NULL,
"familyName" TEXT NOT NULL,
"childName" TEXT NOT NULL,
"date" DATE NOT NULL,
"wholeDay" BOOLEAN NOT NULL DEFAULT true,
"startMinute" INTEGER,
"endMinute" INTEGER,
"expiresAt" TIMESTAMP(3) NOT NULL,
"deadline" TIMESTAMP(3) NOT NULL,
"status" "NotdienstAlarmStatus" NOT NULL DEFAULT 'PENDING',
"triggeredById" TEXT,
"triggeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"respondedById" TEXT,
"respondedAt" TIMESTAMP(3),
"responseSource" TEXT,
"escalatedAt" TIMESTAMP(3),
"educatorId" TEXT,
"notes" TEXT,
"cancelledAt" TIMESTAMP(3),
"cancelReason" TEXT,
CONSTRAINT "notdienst_alarms_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notdienst_alarm_recipients" (
"id" TEXT NOT NULL,
"alarmId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"sessionVersion" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "notdienst_alarm_recipients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notdienst_alarm_notifications" (
"id" TEXT NOT NULL,
"alarmId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"eventKey" TEXT NOT NULL,
"message" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"readAt" TIMESTAMP(3),
CONSTRAINT "notdienst_alarm_notifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notdienst_alarm_emails" (
"id" TEXT NOT NULL,
"alarmId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"eventKey" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"message" TEXT NOT NULL,
"path" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"sentAt" TIMESTAMP(3),
"skippedAt" TIMESTAMP(3),
"leaseUntil" TIMESTAMP(3),
"failed" BOOLEAN NOT NULL DEFAULT false,
"attempts" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "notdienst_alarm_emails_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notdienst_alarm_history" (
"id" TEXT NOT NULL,
"alarmId" TEXT NOT NULL,
"actorId" TEXT,
"action" TEXT NOT NULL,
"details" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notdienst_alarm_history_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "staff_profiles_educatorId_key" ON "staff_profiles"("educatorId");
-- CreateIndex
CREATE UNIQUE INDEX "staff_profiles_userId_key" ON "staff_profiles"("userId");
-- CreateIndex
CREATE INDEX "staff_profiles_kitaId_idx" ON "staff_profiles"("kitaId");
-- CreateIndex
CREATE INDEX "staff_emails_profileId_sentAt_skippedAt_idx" ON "staff_emails"("profileId", "sentAt", "skippedAt");
-- CreateIndex
CREATE UNIQUE INDEX "staff_work_patterns_profileId_effectiveFrom_key" ON "staff_work_patterns"("profileId", "effectiveFrom");
-- CreateIndex
CREATE UNIQUE INDEX "staff_day_overrides_profileId_date_key" ON "staff_day_overrides"("profileId", "date");
-- CreateIndex
CREATE UNIQUE INDEX "staff_holidays_kitaId_date_key" ON "staff_holidays"("kitaId", "date");
-- CreateIndex
CREATE UNIQUE INDEX "staff_leave_accounts_profileId_year_key" ON "staff_leave_accounts"("profileId", "year");
-- CreateIndex
CREATE INDEX "staff_leaves_profileId_status_startDate_idx" ON "staff_leaves"("profileId", "status", "startDate");
-- CreateIndex
CREATE UNIQUE INDEX "staff_leave_days_leaveId_date_key" ON "staff_leave_days"("leaveId", "date");
-- CreateIndex
CREATE INDEX "staff_time_entries_profileId_date_idx" ON "staff_time_entries"("profileId", "date");
-- CreateIndex
CREATE UNIQUE INDEX "staff_weeks_profileId_weekStart_key" ON "staff_weeks"("profileId", "weekStart");
-- CreateIndex
CREATE INDEX "staff_adjustments_profileId_date_idx" ON "staff_adjustments"("profileId", "date");
-- CreateIndex
CREATE INDEX "staff_history_profileId_createdAt_idx" ON "staff_history"("profileId", "createdAt");
-- CreateIndex
CREATE INDEX "staff_notifications_userId_readAt_createdAt_idx" ON "staff_notifications"("userId", "readAt", "createdAt");
-- CreateIndex
CREATE INDEX "family_event_participations_kitaId_idx" ON "family_event_participations"("kitaId");
-- CreateIndex
CREATE INDEX "family_event_participations_familyId_idx" ON "family_event_participations"("familyId");
-- CreateIndex
CREATE UNIQUE INDEX "family_event_participations_eventId_familyId_key" ON "family_event_participations"("eventId", "familyId");
-- CreateIndex
CREATE UNIQUE INDEX "notdienst_alarms_activeAssignmentId_key" ON "notdienst_alarms"("activeAssignmentId");
-- CreateIndex
CREATE INDEX "notdienst_alarms_kitaId_date_idx" ON "notdienst_alarms"("kitaId", "date");
-- CreateIndex
CREATE INDEX "notdienst_alarms_status_deadline_idx" ON "notdienst_alarms"("status", "deadline");
-- CreateIndex
CREATE UNIQUE INDEX "notdienst_alarm_recipients_token_key" ON "notdienst_alarm_recipients"("token");
-- CreateIndex
CREATE UNIQUE INDEX "notdienst_alarm_recipients_alarmId_userId_key" ON "notdienst_alarm_recipients"("alarmId", "userId");
-- CreateIndex
CREATE INDEX "notdienst_alarm_notifications_userId_readAt_idx" ON "notdienst_alarm_notifications"("userId", "readAt");
-- CreateIndex
CREATE UNIQUE INDEX "notdienst_alarm_notifications_alarmId_userId_eventKey_key" ON "notdienst_alarm_notifications"("alarmId", "userId", "eventKey");
-- CreateIndex
CREATE INDEX "notdienst_alarm_emails_sentAt_skippedAt_createdAt_idx" ON "notdienst_alarm_emails"("sentAt", "skippedAt", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "notdienst_alarm_emails_alarmId_userId_eventKey_key" ON "notdienst_alarm_emails"("alarmId", "userId", "eventKey");
-- CreateIndex
CREATE INDEX "notdienst_alarm_history_alarmId_createdAt_idx" ON "notdienst_alarm_history"("alarmId", "createdAt");
-- AddForeignKey
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_educatorId_fkey" FOREIGN KEY ("educatorId") REFERENCES "educators"("id") ON DELETE NO ACTION ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_work_patterns" ADD CONSTRAINT "staff_work_patterns_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_day_overrides" ADD CONSTRAINT "staff_day_overrides_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_holidays" ADD CONSTRAINT "staff_holidays_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_leave_accounts" ADD CONSTRAINT "staff_leave_accounts_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_leaves" ADD CONSTRAINT "staff_leaves_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_leave_days" ADD CONSTRAINT "staff_leave_days_leaveId_fkey" FOREIGN KEY ("leaveId") REFERENCES "staff_leaves"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_time_entries" ADD CONSTRAINT "staff_time_entries_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_weeks" ADD CONSTRAINT "staff_weeks_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_adjustments" ADD CONSTRAINT "staff_adjustments_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_history" ADD CONSTRAINT "staff_history_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_notifications" ADD CONSTRAINT "staff_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "staff_notifications" ADD CONSTRAINT "staff_notifications_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "termine"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "families"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "notdienst_assignments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "families"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_recipients" ADD CONSTRAINT "notdienst_alarm_recipients_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_recipients" ADD CONSTRAINT "notdienst_alarm_recipients_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_notifications" ADD CONSTRAINT "notdienst_alarm_notifications_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_notifications" ADD CONSTRAINT "notdienst_alarm_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_emails" ADD CONSTRAINT "notdienst_alarm_emails_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_emails" ADD CONSTRAINT "notdienst_alarm_emails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alarm_history" ADD CONSTRAINT "notdienst_alarm_history_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notdienst_alerts" ADD CONSTRAINT "notdienst_alerts_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE SET NULL ON UPDATE CASCADE;
COMMIT;
@@ -0,0 +1,13 @@
BEGIN;
-- Additional tenant invariants intentionally supplement Prisma relations.
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_educatorId_tenant_fkey" FOREIGN KEY ("educatorId", "kitaId") REFERENCES "educators"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_userId_tenant_fkey" FOREIGN KEY ("userId", "kitaId") REFERENCES "users"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_eventId_tenant_fkey" FOREIGN KEY ("eventId", "kitaId") REFERENCES "termine"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_familyId_tenant_fkey" FOREIGN KEY ("familyId", "kitaId") REFERENCES "families"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_familyId_tenant_fkey" FOREIGN KEY ("familyId", "kitaId") REFERENCES "families"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_assignmentId_tenant_fkey" FOREIGN KEY ("assignmentId", "kitaId") REFERENCES "notdienst_assignments"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_activeAssignmentId_tenant_fkey" FOREIGN KEY ("activeAssignmentId", "kitaId") REFERENCES "notdienst_assignments"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_triggeredById_tenant_fkey" FOREIGN KEY ("triggeredById", "kitaId") REFERENCES "users"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_respondedById_tenant_fkey" FOREIGN KEY ("respondedById", "kitaId") REFERENCES "users"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_educatorId_tenant_fkey" FOREIGN KEY ("educatorId", "kitaId") REFERENCES "educators"("id", "kitaId") ON DELETE NO ACTION ON UPDATE NO ACTION;
COMMIT;
+418 -29
View File
@@ -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])
+44
View File
@@ -0,0 +1,44 @@
-- CreateEnum
CREATE TYPE "RsvpMode" AS ENUM ('CHILD', 'FAMILY');
-- CreateEnum
CREATE TYPE "AnnouncementAudience" AS ENUM ('ELTERN', 'ERZIEHER', 'ALLE');
-- AlterTable
ALTER TABLE "announcements" ADD COLUMN "audience" "AnnouncementAudience" NOT NULL DEFAULT 'ALLE';
-- AlterTable
ALTER TABLE "termine" ADD COLUMN "rsvpMode" "RsvpMode" NOT NULL DEFAULT 'CHILD';
-- CreateTable
CREATE TABLE "family_event_participations" (
"id" TEXT NOT NULL,
"kitaId" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"familyId" TEXT NOT NULL,
"status" "EventParticipationStatus" NOT NULL,
"adultCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "family_event_participations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "family_event_participations_kitaId_idx" ON "family_event_participations"("kitaId");
-- CreateIndex
CREATE INDEX "family_event_participations_familyId_idx" ON "family_event_participations"("familyId");
-- CreateIndex
CREATE UNIQUE INDEX "family_event_participations_eventId_familyId_key" ON "family_event_participations"("eventId", "familyId");
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "termine"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "family_event_participations" ADD CONSTRAINT "family_event_participations_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "families"("id") ON DELETE CASCADE ON UPDATE CASCADE;