Add non-destructive dev seed on startup
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
// =====================================================================
|
||||
// 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
|
||||
ELTERN
|
||||
}
|
||||
|
||||
enum InvitationStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
EXPIRED
|
||||
REVOKED
|
||||
}
|
||||
|
||||
enum TerminType {
|
||||
KITA_FEST
|
||||
SCHLIESSTAG
|
||||
TEAMTAG
|
||||
MITMACH_TAG
|
||||
ELTERNABEND
|
||||
MITGLIEDERVERSAMMLUNG
|
||||
ELTERNCAFE
|
||||
GEBURTSTAG_INTERN
|
||||
GEBURTSTAG_EXTERN
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
enum TerminStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
REJECTED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum NotdienstPlanStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
}
|
||||
|
||||
enum NotdienstAlertStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 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[]
|
||||
children Child[]
|
||||
educators Educator[]
|
||||
parentDuties ParentDuty[]
|
||||
parentDutyAssignments ParentDutyAssignment[]
|
||||
invitations Invitation[]
|
||||
termine Termin[]
|
||||
mitbringselItems MitbringselItem[]
|
||||
notdienstPlans NotdienstPlan[]
|
||||
notdienstAvailabilities NotdienstAvailability[]
|
||||
notdienstAssignments NotdienstAssignment[]
|
||||
notdienstAlerts NotdienstAlert[]
|
||||
childParents ChildParent[]
|
||||
|
||||
@@map("kitas")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 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)
|
||||
|
||||
email String @unique
|
||||
passwordHash String
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
|
||||
role UserRole @default(ELTERN)
|
||||
|
||||
// Adressbuch / Kontaktdaten (nur sichtbar bei directoryOptIn)
|
||||
phone String?
|
||||
street String?
|
||||
postalCode String?
|
||||
city String?
|
||||
|
||||
// ---- DSGVO Consent Logging (planung.md §5.2) ----
|
||||
/// Zeitstempel der Annahme der Datenschutzerklärung.
|
||||
/// Ohne diesen Wert blockiert die App den Zugriff (Redirect → Onboarding).
|
||||
privacyPolicyAcceptedAt DateTime?
|
||||
/// Versionsstring der akzeptierten Datenschutzerklärung (z.B. "2025-04-01").
|
||||
privacyPolicyVersion String?
|
||||
/// Zeitstempel des Opt-Ins für das interne Kita-Adressbuch (Modul 3).
|
||||
directoryOptInAt DateTime?
|
||||
|
||||
// Auth-Bookkeeping
|
||||
emailVerifiedAt DateTime?
|
||||
lastLoginAt DateTime?
|
||||
failedLoginAttempts Int @default(0)
|
||||
lockedUntil DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
childLinks ChildParent[]
|
||||
dutyAssignments ParentDutyAssignment[]
|
||||
|
||||
notdienstAvailabilities NotdienstAvailability[]
|
||||
notdienstAlertsAssigned NotdienstAlert[] @relation("NotdienstAlertParent")
|
||||
notdienstAlertsTriggered NotdienstAlert[] @relation("NotdienstAlertTrigger")
|
||||
notdienstPlansCreated NotdienstPlan[] @relation("NotdienstPlanCreator")
|
||||
|
||||
termineCreated Termin[] @relation("TerminCreator")
|
||||
termineApproved Termin[] @relation("TerminApprover")
|
||||
|
||||
mitbringselItems MitbringselItem[]
|
||||
invitationsCreated Invitation[] @relation("InvitationCreator")
|
||||
|
||||
@@index([kitaId])
|
||||
@@index([kitaId, role])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// CHILDREN
|
||||
// =====================================================================
|
||||
|
||||
model Child {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
dateOfBirth DateTime?
|
||||
notes String?
|
||||
|
||||
active Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
parentLinks ChildParent[]
|
||||
notdienstAvailabilities NotdienstAvailability[]
|
||||
notdienstAssignments NotdienstAssignment[]
|
||||
|
||||
@@index([kitaId])
|
||||
@@map("children")
|
||||
}
|
||||
|
||||
/// Verknüpfung Kind ↔ Elternteil (m:n).
|
||||
/// Kaskadiert über beide Seiten, damit das Löschen eines Users
|
||||
/// oder Kindes keine Datenleichen hinterlässt.
|
||||
model ChildParent {
|
||||
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)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([childId, userId])
|
||||
@@index([kitaId])
|
||||
@@index([userId])
|
||||
@@map("child_parents")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// EDUCATORS (ErzieherInnen — reine Stammdaten, keine Logins, Modul 4)
|
||||
// =====================================================================
|
||||
|
||||
model Educator {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
active Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
notdienstAlerts NotdienstAlert[]
|
||||
|
||||
@@index([kitaId])
|
||||
@@map("educators")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// PARENT DUTIES (Feste Ämter / Elterndienste, Modul 3)
|
||||
// =====================================================================
|
||||
|
||||
model ParentDuty {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
description String?
|
||||
active Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
assignments ParentDutyAssignment[]
|
||||
|
||||
@@unique([kitaId, name])
|
||||
@@index([kitaId])
|
||||
@@map("parent_duties")
|
||||
}
|
||||
|
||||
model ParentDutyAssignment {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
dutyId String
|
||||
duty ParentDuty @relation(fields: [dutyId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([dutyId, userId])
|
||||
@@index([kitaId])
|
||||
@@index([userId])
|
||||
@@map("parent_duty_assignments")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// INVITATIONS (Invite-Only Onboarding, Modul 3)
|
||||
// =====================================================================
|
||||
|
||||
model Invitation {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
email String
|
||||
role UserRole @default(ELTERN)
|
||||
|
||||
token String @unique
|
||||
status InvitationStatus @default(PENDING)
|
||||
expiresAt DateTime
|
||||
acceptedAt DateTime?
|
||||
|
||||
invitedById String?
|
||||
invitedBy User? @relation("InvitationCreator", fields: [invitedById], references: [id], onDelete: SetNull)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([kitaId, email])
|
||||
@@index([kitaId])
|
||||
@@map("invitations")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TERMINE (Kalender, Modul 2)
|
||||
// =====================================================================
|
||||
|
||||
model Termin {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
title String
|
||||
description String?
|
||||
type TerminType
|
||||
status TerminStatus @default(PENDING)
|
||||
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
allDay Boolean @default(false)
|
||||
|
||||
/// Mitbringliste pro Termin aktivierbar (Modul 2, "Flexible Mitbring-Listen")
|
||||
mitbringselListEnabled Boolean @default(false)
|
||||
|
||||
createdById String?
|
||||
createdBy User? @relation("TerminCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
||||
|
||||
approvedById String?
|
||||
approvedBy User? @relation("TerminApprover", fields: [approvedById], references: [id], onDelete: SetNull)
|
||||
approvedAt DateTime?
|
||||
rejectionReason String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
mitbringselItems MitbringselItem[]
|
||||
|
||||
@@index([kitaId, startDate])
|
||||
@@index([kitaId, status])
|
||||
@@map("termine")
|
||||
}
|
||||
|
||||
model MitbringselItem {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
terminId String
|
||||
termin Termin @relation(fields: [terminId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Cascade: Beim Löschen eines Accounts werden dessen Mitbringsel-Einträge
|
||||
/// rückstandslos mitgelöscht (DSGVO "Recht auf Vergessenwerden").
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
content String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([kitaId])
|
||||
@@index([terminId])
|
||||
@@map("mitbringsel_items")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// NOTDIENST (Modul 1 – Kern-Feature)
|
||||
// =====================================================================
|
||||
|
||||
/// Monatlicher Notdienst-Plan. Erst DRAFT (Koordinator bearbeitet),
|
||||
/// nach "Veröffentlichung" PUBLISHED.
|
||||
model NotdienstPlan {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
year Int
|
||||
/// Monat 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[]
|
||||
|
||||
@@unique([planId, date])
|
||||
@@index([kitaId, date])
|
||||
@@map("notdienst_assignments")
|
||||
}
|
||||
|
||||
/// Aktive Alarmierung — wird bei Krankmeldung einer Fachkraft
|
||||
/// vom Koordinator ausgelöst. Bestätigung via Magic-Link (Token).
|
||||
model NotdienstAlert {
|
||||
id String @id @default(cuid())
|
||||
kitaId String
|
||||
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
|
||||
|
||||
assignmentId String
|
||||
assignment NotdienstAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Eingeteiltes Elternteil (Empfänger des Alarms).
|
||||
parentUserId String
|
||||
parentUser User @relation("NotdienstAlertParent", fields: [parentUserId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Auslösender Koordinator.
|
||||
triggeredById String?
|
||||
triggeredBy User? @relation("NotdienstAlertTrigger", fields: [triggeredById], references: [id], onDelete: SetNull)
|
||||
|
||||
/// Optional: kranke Fachkraft (Referenz für Reporting).
|
||||
educatorId String?
|
||||
educator Educator? @relation(fields: [educatorId], references: [id], onDelete: SetNull)
|
||||
|
||||
status NotdienstAlertStatus @default(PENDING)
|
||||
|
||||
/// Einmal-Token für den Bestätigungslink in der Alarm-Mail.
|
||||
confirmationToken String @unique
|
||||
triggeredAt DateTime @default(now())
|
||||
confirmedAt DateTime?
|
||||
|
||||
notes String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([kitaId])
|
||||
@@index([assignmentId])
|
||||
@@index([parentUserId])
|
||||
@@map("notdienst_alerts")
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// AUTH-HILFSTABELLE
|
||||
// =====================================================================
|
||||
|
||||
/// Tokens für Passwort-Reset und E-Mail-Verifikation.
|
||||
/// Kompatibel mit dem NextAuth-Schema, falls später Email-Provider aktiviert wird.
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@map("verification_tokens")
|
||||
}
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
import {
|
||||
InvitationStatus,
|
||||
NotdienstAlertStatus,
|
||||
NotdienstPlanStatus,
|
||||
PrismaClient,
|
||||
TerminStatus,
|
||||
TerminType,
|
||||
UserRole,
|
||||
} from "@prisma/client";
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
// =====================================================================
|
||||
// Kita-Planer · Dev-Seed
|
||||
// ---------------------------------------------------------------------
|
||||
// Default: nicht destruktiv. Seed-Daten werden nur angelegt, wenn die DB
|
||||
// noch keine Kita und keine User enthaelt.
|
||||
//
|
||||
// Reset: `npm run db:seed:reset` loescht gezielt die Demo-Daten und baut
|
||||
// sie neu auf. Production wird nur mit ALLOW_DATABASE_SEED=true erlaubt.
|
||||
// =====================================================================
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const DEMO_KITA_SLUG = "waldameisen";
|
||||
const DEFAULT_PASSWORD = "password123";
|
||||
const PRIVACY_POLICY_VERSION = "2026-05-01";
|
||||
const RESET_MODE = process.argv.includes("--reset");
|
||||
|
||||
const DEMO_USER_EMAILS = [
|
||||
"super@kita-planer.local",
|
||||
"admin@waldameisen.local",
|
||||
"mueller@waldameisen.local",
|
||||
"schmidt@waldameisen.local",
|
||||
"yilmaz@waldameisen.local",
|
||||
"pending@waldameisen.local",
|
||||
];
|
||||
|
||||
const DEMO_VERIFICATION_TOKENS = [
|
||||
"demo-pending-parent-token",
|
||||
"demo-invite-token",
|
||||
];
|
||||
|
||||
type SeedContext = Awaited<ReturnType<typeof createDemoUsers>>;
|
||||
|
||||
function dateOnly(date: Date) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number) {
|
||||
const next = new Date(date);
|
||||
next.setDate(next.getDate() + days);
|
||||
return dateOnly(next);
|
||||
}
|
||||
|
||||
function startOfMonth(date: Date) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
}
|
||||
|
||||
function getPreviousMonth(date: Date) {
|
||||
return new Date(date.getFullYear(), date.getMonth() - 1, 1);
|
||||
}
|
||||
|
||||
function getLastDayOfPreviousMonth(date: Date) {
|
||||
return dateOnly(new Date(date.getFullYear(), date.getMonth(), 0));
|
||||
}
|
||||
|
||||
function getTargetMonth(date: Date) {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 1);
|
||||
}
|
||||
|
||||
function getWorkingDays(year: number, month: number, limit?: number) {
|
||||
const days: Date[] = [];
|
||||
const cursor = new Date(year, month - 1, 1);
|
||||
|
||||
while (cursor.getMonth() === month - 1) {
|
||||
const day = cursor.getDay();
|
||||
if (day !== 0 && day !== 6) {
|
||||
days.push(dateOnly(cursor));
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
return typeof limit === "number" ? days.slice(0, limit) : days;
|
||||
}
|
||||
|
||||
function isSeedAllowed() {
|
||||
if (
|
||||
process.env.NODE_ENV === "development" ||
|
||||
process.env.ALLOW_DATABASE_SEED === "true"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Seed uebersprungen: NODE_ENV ist nicht development. Setze ALLOW_DATABASE_SEED=true, wenn das bewusst gewuenscht ist.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function isDatabaseEmpty() {
|
||||
const [kitaCount, userCount] = await Promise.all([
|
||||
prisma.kita.count(),
|
||||
prisma.user.count(),
|
||||
]);
|
||||
|
||||
return kitaCount === 0 && userCount === 0;
|
||||
}
|
||||
|
||||
async function resetDemoData() {
|
||||
const demoUsers = await prisma.user.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ email: { in: DEMO_USER_EMAILS } },
|
||||
{ kita: { slug: DEMO_KITA_SLUG } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.verificationToken.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ token: { in: DEMO_VERIFICATION_TOKENS } },
|
||||
{ identifier: { in: demoUsers.map((user) => user.id) } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.kita.deleteMany({ where: { slug: DEMO_KITA_SLUG } });
|
||||
await prisma.user.deleteMany({ where: { email: { in: DEMO_USER_EMAILS } } });
|
||||
}
|
||||
|
||||
async function createDemoUsers(passwordHash: string, consentAt: Date) {
|
||||
const superAdmin = await prisma.user.create({
|
||||
data: {
|
||||
email: "super@kita-planer.local",
|
||||
passwordHash,
|
||||
firstName: "Sys",
|
||||
lastName: "Admin",
|
||||
role: UserRole.SUPERADMIN,
|
||||
privacyPolicyAcceptedAt: consentAt,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
emailVerifiedAt: consentAt,
|
||||
},
|
||||
});
|
||||
|
||||
const kita = await prisma.kita.create({
|
||||
data: {
|
||||
name: "Waldameisen e.V.",
|
||||
slug: DEMO_KITA_SLUG,
|
||||
notdienstModuleEnabled: true,
|
||||
terminModuleEnabled: true,
|
||||
adressbuchModuleEnabled: true,
|
||||
notdienstMinPerChildPerMonth: 2,
|
||||
notdienstReminderDaysBefore: 7,
|
||||
},
|
||||
});
|
||||
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "admin@waldameisen.local",
|
||||
passwordHash,
|
||||
firstName: "Anna",
|
||||
lastName: "Vorstand",
|
||||
role: UserRole.ADMIN,
|
||||
privacyPolicyAcceptedAt: consentAt,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
directoryOptInAt: consentAt,
|
||||
emailVerifiedAt: consentAt,
|
||||
phone: "+49 30 1000 1000",
|
||||
street: "Kitaweg 1",
|
||||
postalCode: "10115",
|
||||
city: "Berlin",
|
||||
},
|
||||
});
|
||||
|
||||
const koordinator = await prisma.user.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "mueller@waldameisen.local",
|
||||
passwordHash,
|
||||
firstName: "Maria",
|
||||
lastName: "Mueller",
|
||||
role: UserRole.KOORDINATOR,
|
||||
privacyPolicyAcceptedAt: consentAt,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
directoryOptInAt: consentAt,
|
||||
emailVerifiedAt: consentAt,
|
||||
phone: "+49 30 1234 5678",
|
||||
street: "Beispielweg 1",
|
||||
postalCode: "10115",
|
||||
city: "Berlin",
|
||||
},
|
||||
});
|
||||
|
||||
const elternSchmidt = await prisma.user.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "schmidt@waldameisen.local",
|
||||
passwordHash,
|
||||
firstName: "Lukas",
|
||||
lastName: "Schmidt",
|
||||
role: UserRole.ELTERN,
|
||||
privacyPolicyAcceptedAt: consentAt,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
emailVerifiedAt: consentAt,
|
||||
},
|
||||
});
|
||||
|
||||
const elternYilmaz = await prisma.user.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "yilmaz@waldameisen.local",
|
||||
passwordHash,
|
||||
firstName: "Aylin",
|
||||
lastName: "Yilmaz",
|
||||
role: UserRole.ELTERN,
|
||||
privacyPolicyAcceptedAt: consentAt,
|
||||
privacyPolicyVersion: PRIVACY_POLICY_VERSION,
|
||||
directoryOptInAt: consentAt,
|
||||
emailVerifiedAt: consentAt,
|
||||
phone: "+49 30 9876 5432",
|
||||
street: "Parkstrasse 8",
|
||||
postalCode: "10405",
|
||||
city: "Berlin",
|
||||
},
|
||||
});
|
||||
|
||||
const pendingParent = await prisma.user.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "pending@waldameisen.local",
|
||||
passwordHash: "",
|
||||
firstName: "Lena",
|
||||
lastName: "Fischer",
|
||||
role: UserRole.ELTERN,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
kita,
|
||||
superAdmin,
|
||||
admin,
|
||||
koordinator,
|
||||
elternSchmidt,
|
||||
elternYilmaz,
|
||||
pendingParent,
|
||||
};
|
||||
}
|
||||
|
||||
async function createChildren({
|
||||
kita,
|
||||
koordinator,
|
||||
elternSchmidt,
|
||||
elternYilmaz,
|
||||
pendingParent,
|
||||
}: SeedContext) {
|
||||
const anna = await prisma.child.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
firstName: "Anna",
|
||||
lastName: "Mueller",
|
||||
dateOfBirth: new Date("2021-03-15"),
|
||||
parentLinks: {
|
||||
create: { kitaId: kita.id, userId: koordinator.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ben = await prisma.child.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
firstName: "Ben",
|
||||
lastName: "Mueller",
|
||||
dateOfBirth: new Date("2023-07-22"),
|
||||
notes: "Geschwisterkind von Anna.",
|
||||
parentLinks: {
|
||||
create: { kitaId: kita.id, userId: koordinator.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const clara = await prisma.child.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
firstName: "Clara",
|
||||
lastName: "Schmidt",
|
||||
dateOfBirth: new Date("2022-11-03"),
|
||||
parentLinks: {
|
||||
create: { kitaId: kita.id, userId: elternSchmidt.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emil = await prisma.child.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
firstName: "Emil",
|
||||
lastName: "Yilmaz",
|
||||
dateOfBirth: new Date("2021-09-09"),
|
||||
parentLinks: {
|
||||
create: [
|
||||
{ kitaId: kita.id, userId: elternYilmaz.id },
|
||||
{ kitaId: kita.id, userId: elternSchmidt.id },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const nina = await prisma.child.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
firstName: "Nina",
|
||||
lastName: "Fischer",
|
||||
dateOfBirth: new Date("2022-05-30"),
|
||||
parentLinks: {
|
||||
create: { kitaId: kita.id, userId: pendingParent.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { anna, ben, clara, emil, nina };
|
||||
}
|
||||
|
||||
async function createEducators(kitaId: string) {
|
||||
const sabine = await prisma.educator.create({
|
||||
data: { kitaId, firstName: "Sabine", lastName: "Schulze" },
|
||||
});
|
||||
const petra = await prisma.educator.create({
|
||||
data: { kitaId, firstName: "Petra", lastName: "Klein" },
|
||||
});
|
||||
const jonas = await prisma.educator.create({
|
||||
data: { kitaId, firstName: "Jonas", lastName: "Wagner" },
|
||||
});
|
||||
const norbert = await prisma.educator.create({
|
||||
data: {
|
||||
kitaId,
|
||||
firstName: "Norbert",
|
||||
lastName: "Altmann",
|
||||
active: false,
|
||||
},
|
||||
});
|
||||
|
||||
return { sabine, petra, jonas, norbert };
|
||||
}
|
||||
|
||||
async function createParentDuties({
|
||||
kita,
|
||||
koordinator,
|
||||
elternSchmidt,
|
||||
elternYilmaz,
|
||||
}: SeedContext) {
|
||||
const waesche = await prisma.parentDuty.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
name: "Waeschedienst",
|
||||
description: "Kita-Waesche waschen und zurueckbringen.",
|
||||
},
|
||||
});
|
||||
|
||||
const einkauf = await prisma.parentDuty.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
name: "Einkauf",
|
||||
description: "Woechentlicher Einkauf fuer die Kita-Kueche.",
|
||||
},
|
||||
});
|
||||
|
||||
const garten = await prisma.parentDuty.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
name: "Gartendienst",
|
||||
description: "Aussenbereich pflegen und Spielmaterial pruefen.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.parentDutyAssignment.createMany({
|
||||
data: [
|
||||
{ kitaId: kita.id, dutyId: waesche.id, userId: elternSchmidt.id },
|
||||
{ kitaId: kita.id, dutyId: einkauf.id, userId: koordinator.id },
|
||||
{ kitaId: kita.id, dutyId: garten.id, userId: elternYilmaz.id },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function createInvites({ kita, admin, pendingParent }: SeedContext) {
|
||||
const expires = addDays(new Date(), 7);
|
||||
|
||||
await prisma.invitation.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
email: "invite@waldameisen.local",
|
||||
role: UserRole.ELTERN,
|
||||
token: "demo-invite-token",
|
||||
status: InvitationStatus.PENDING,
|
||||
expiresAt: expires,
|
||||
invitedById: admin.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier: pendingParent.id,
|
||||
token: "demo-pending-parent-token",
|
||||
expires,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function createTermine({
|
||||
kita,
|
||||
admin,
|
||||
koordinator,
|
||||
elternSchmidt,
|
||||
elternYilmaz,
|
||||
}: SeedContext) {
|
||||
const now = new Date();
|
||||
|
||||
const elternabend = await prisma.termin.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
title: "Elternabend Fruehling",
|
||||
description: "Austausch zu Terminen, Notdienst und Sommerplanung.",
|
||||
type: TerminType.ELTERNABEND,
|
||||
status: TerminStatus.CONFIRMED,
|
||||
startDate: addDays(now, 10),
|
||||
endDate: addDays(now, 10),
|
||||
allDay: false,
|
||||
mitbringselListEnabled: true,
|
||||
createdById: admin.id,
|
||||
approvedById: admin.id,
|
||||
approvedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.termin.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
title: "Teamtag",
|
||||
description: "Die Kita bleibt wegen interner Fortbildung geschlossen.",
|
||||
type: TerminType.TEAMTAG,
|
||||
status: TerminStatus.CONFIRMED,
|
||||
startDate: addDays(now, 18),
|
||||
endDate: addDays(now, 18),
|
||||
allDay: true,
|
||||
createdById: admin.id,
|
||||
approvedById: admin.id,
|
||||
approvedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.termin.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
title: "Elterncafe im Garten",
|
||||
description: "Anfrage eines Elternteils, noch nicht bestaetigt.",
|
||||
type: TerminType.ELTERNCAFE,
|
||||
status: TerminStatus.PENDING,
|
||||
startDate: addDays(now, 24),
|
||||
endDate: addDays(now, 24),
|
||||
allDay: false,
|
||||
createdById: elternSchmidt.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.termin.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
title: "Spontaner Ausflug",
|
||||
description: "Beispiel fuer eine abgelehnte Terminanfrage.",
|
||||
type: TerminType.SONSTIGES,
|
||||
status: TerminStatus.REJECTED,
|
||||
startDate: addDays(now, -6),
|
||||
endDate: addDays(now, -6),
|
||||
allDay: true,
|
||||
createdById: elternYilmaz.id,
|
||||
approvedById: koordinator.id,
|
||||
approvedAt: addDays(now, -5),
|
||||
rejectionReason: "Zu kurzfristig fuer die Gruppe.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.mitbringselItem.createMany({
|
||||
data: [
|
||||
{
|
||||
kitaId: kita.id,
|
||||
terminId: elternabend.id,
|
||||
userId: koordinator.id,
|
||||
content: "Obstplatte",
|
||||
},
|
||||
{
|
||||
kitaId: kita.id,
|
||||
terminId: elternabend.id,
|
||||
userId: elternSchmidt.id,
|
||||
content: "Brot und Aufstrich",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function createNotdienstData(
|
||||
{ kita, koordinator, elternSchmidt, elternYilmaz }: SeedContext,
|
||||
children: Awaited<ReturnType<typeof createChildren>>,
|
||||
educators: Awaited<ReturnType<typeof createEducators>>,
|
||||
) {
|
||||
const today = dateOnly(new Date());
|
||||
const currentMonth = startOfMonth(today);
|
||||
const previousMonth = getPreviousMonth(today);
|
||||
const previousAssignmentDate = getLastDayOfPreviousMonth(today);
|
||||
const targetMonth = getTargetMonth(today);
|
||||
|
||||
const availabilityPairs = [
|
||||
{ childId: children.anna.id, userId: koordinator.id },
|
||||
{ childId: children.clara.id, userId: elternSchmidt.id },
|
||||
{ childId: children.emil.id, userId: elternYilmaz.id },
|
||||
{ childId: children.ben.id, userId: koordinator.id },
|
||||
];
|
||||
|
||||
const targetWorkingDays = getWorkingDays(
|
||||
targetMonth.getFullYear(),
|
||||
targetMonth.getMonth() + 1,
|
||||
12,
|
||||
);
|
||||
|
||||
await prisma.notdienstAvailability.createMany({
|
||||
data: targetWorkingDays.map((date, index) => {
|
||||
const pair = availabilityPairs[index % availabilityPairs.length];
|
||||
return {
|
||||
kitaId: kita.id,
|
||||
childId: pair.childId,
|
||||
userId: pair.userId,
|
||||
date,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const currentPlan = await prisma.notdienstPlan.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
year: currentMonth.getFullYear(),
|
||||
month: currentMonth.getMonth() + 1,
|
||||
status: NotdienstPlanStatus.PUBLISHED,
|
||||
createdById: koordinator.id,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.notdienstAssignment.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
planId: currentPlan.id,
|
||||
childId: children.anna.id,
|
||||
date: today,
|
||||
},
|
||||
});
|
||||
|
||||
const previousPlan = await prisma.notdienstPlan.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
year: previousMonth.getFullYear(),
|
||||
month: previousMonth.getMonth() + 1,
|
||||
status: NotdienstPlanStatus.PUBLISHED,
|
||||
createdById: koordinator.id,
|
||||
publishedAt: addDays(today, -10),
|
||||
},
|
||||
});
|
||||
|
||||
const pastAssignment = await prisma.notdienstAssignment.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
planId: previousPlan.id,
|
||||
childId: children.clara.id,
|
||||
date: previousAssignmentDate,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.notdienstAlert.create({
|
||||
data: {
|
||||
kitaId: kita.id,
|
||||
assignmentId: pastAssignment.id,
|
||||
parentUserId: elternSchmidt.id,
|
||||
triggeredById: koordinator.id,
|
||||
educatorId: educators.sabine.id,
|
||||
status: NotdienstAlertStatus.CONFIRMED,
|
||||
confirmationToken: "demo-confirmed-alert-token",
|
||||
triggeredAt: previousAssignmentDate,
|
||||
confirmedAt: previousAssignmentDate,
|
||||
notes: "Beispiel-Alarm aus Seed-Daten.",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function createDemoData() {
|
||||
console.log("Seeding Kita-Planer demo data...");
|
||||
|
||||
const passwordHash = await hash(DEFAULT_PASSWORD, 12);
|
||||
const consentAt = new Date();
|
||||
const context = await createDemoUsers(passwordHash, consentAt);
|
||||
const children = await createChildren(context);
|
||||
const educators = await createEducators(context.kita.id);
|
||||
|
||||
await createParentDuties(context);
|
||||
await createInvites(context);
|
||||
await createTermine(context);
|
||||
await createNotdienstData(context, children, educators);
|
||||
|
||||
printSummary(context, children);
|
||||
}
|
||||
|
||||
async function seedIfEmpty() {
|
||||
if (!(await isDatabaseEmpty())) {
|
||||
console.log("Seed uebersprungen: Datenbank enthaelt bereits Kita- oder User-Daten.");
|
||||
return;
|
||||
}
|
||||
|
||||
await createDemoData();
|
||||
}
|
||||
|
||||
function printSummary(
|
||||
{
|
||||
kita,
|
||||
superAdmin,
|
||||
admin,
|
||||
koordinator,
|
||||
elternSchmidt,
|
||||
elternYilmaz,
|
||||
pendingParent,
|
||||
}: SeedContext,
|
||||
children: Awaited<ReturnType<typeof createChildren>>,
|
||||
) {
|
||||
console.log("Seed complete");
|
||||
console.log("");
|
||||
console.log(` Kita: ${kita.name} (${kita.slug})`);
|
||||
console.log("");
|
||||
console.log(` Logins (Passwort jeweils: ${DEFAULT_PASSWORD})`);
|
||||
console.log(` Superadmin: ${superAdmin.email}`);
|
||||
console.log(` Admin: ${admin.email}`);
|
||||
console.log(` Koordinator: ${koordinator.email}`);
|
||||
console.log(` Eltern: ${elternSchmidt.email}`);
|
||||
console.log(` Eltern: ${elternYilmaz.email}`);
|
||||
console.log("");
|
||||
console.log(
|
||||
` Offener Invite: ${pendingParent.email} -> /invite/demo-pending-parent-token`,
|
||||
);
|
||||
console.log(
|
||||
` Kinder: ${Object.values(children)
|
||||
.map((child) => `${child.firstName} ${child.lastName}`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!isSeedAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RESET_MODE) {
|
||||
console.log("Reset demo seed data...");
|
||||
await resetDemoData();
|
||||
await createDemoData();
|
||||
return;
|
||||
}
|
||||
|
||||
await seedIfEmpty();
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error("Seed failed:", error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user