import { AbsenceReason, 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", "erzieher@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>; 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 addWeeks(date: Date, weeks: number) { return addDays(date, weeks * 7); } function startOfIsoWeek(date: Date) { const normalized = dateOnly(date); const day = normalized.getDay() || 7; return addDays(normalized, 1 - day); } 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 familyMueller = await prisma.family.create({ data: { kitaId: kita.id, name: "Familie Mueller", }, }); const familySchmidtYilmaz = await prisma.family.create({ data: { kitaId: kita.id, name: "Familie Schmidt-Yilmaz", }, }); const familyFischer = await prisma.family.create({ data: { kitaId: kita.id, name: "Familie Fischer", }, }); 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 erzieherUser = await prisma.user.create({ data: { kitaId: kita.id, email: "erzieher@waldameisen.local", passwordHash, firstName: "Eva", lastName: "Erzieherin", role: UserRole.ERZIEHER, privacyPolicyAcceptedAt: consentAt, privacyPolicyVersion: PRIVACY_POLICY_VERSION, emailVerifiedAt: consentAt, phone: "+49 30 1000 2000", street: "Kitaweg 1", postalCode: "10115", city: "Berlin", }, }); const koordinator = await prisma.user.create({ data: { kitaId: kita.id, familyId: familyMueller.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, familyId: familySchmidtYilmaz.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, familyId: familySchmidtYilmaz.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, familyId: familyFischer.id, email: "pending@waldameisen.local", passwordHash: "", firstName: "Lena", lastName: "Fischer", role: UserRole.ELTERN, }, }); return { kita, familyMueller, familySchmidtYilmaz, familyFischer, superAdmin, admin, erzieherUser, koordinator, elternSchmidt, elternYilmaz, pendingParent, }; } async function createChildren({ kita, familyMueller, familySchmidtYilmaz, familyFischer, }: SeedContext) { const anna = await prisma.child.create({ data: { kitaId: kita.id, familyId: familyMueller.id, firstName: "Anna", lastName: "Mueller", dateOfBirth: new Date("2021-03-15"), }, }); const ben = await prisma.child.create({ data: { kitaId: kita.id, familyId: familyMueller.id, firstName: "Ben", lastName: "Mueller", dateOfBirth: new Date("2023-07-22"), notes: "Geschwisterkind von Anna.", }, }); const clara = await prisma.child.create({ data: { kitaId: kita.id, familyId: familySchmidtYilmaz.id, firstName: "Clara", lastName: "Schmidt", dateOfBirth: new Date("2022-11-03"), }, }); const emil = await prisma.child.create({ data: { kitaId: kita.id, familyId: familySchmidtYilmaz.id, firstName: "Emil", lastName: "Yilmaz", dateOfBirth: new Date("2021-09-09"), }, }); const nina = await prisma.child.create({ data: { kitaId: kita.id, familyId: familyFischer.id, firstName: "Nina", lastName: "Fischer", dateOfBirth: new Date("2022-05-30"), }, }); 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 createAbsences( { kita }: SeedContext, children: Awaited>, ) { const today = dateOnly(new Date()); await prisma.absence.createMany({ data: [ { kitaId: kita.id, childId: children.nina.id, startDate: today, endDate: today, reason: AbsenceReason.ILLNESS, note: "Fieber, bleibt heute zuhause.", }, { kitaId: kita.id, childId: children.emil.id, startDate: addDays(today, 1), endDate: addDays(today, 2), reason: AbsenceReason.VACATION, note: "Familienbesuch.", }, ], }); } async function createDutyPlan({ kita, familyMueller, familySchmidtYilmaz, familyFischer, }: SeedContext) { const waesche = await prisma.dutyType.create({ data: { kitaId: kita.id, name: "Waeschedienst", description: "Woechentlicher Dienstplan fuer Kita-Waesche.", }, }); const einkauf = await prisma.dutyType.create({ data: { kitaId: kita.id, name: "Einkauf", description: "Woechentlicher Einkauf nach Kita-Liste.", }, }); const families = [familyMueller, familySchmidtYilmaz, familyFischer]; const currentWeek = startOfIsoWeek(new Date()); await prisma.dutyAssignment.createMany({ data: Array.from({ length: 8 }).flatMap((_, index) => { const startDate = addWeeks(currentWeek, index); const endDate = addDays(startDate, 6); return [ { kitaId: kita.id, dutyTypeId: waesche.id, familyId: families[index % families.length].id, startDate, endDate, }, { kitaId: kita.id, dutyTypeId: einkauf.id, familyId: families[(index + 1) % families.length].id, startDate, endDate, }, ]; }), }); } 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 createAnnouncements({ kita, admin, koordinator, }: SeedContext) { const sommerfest = await prisma.announcement.create({ data: { kitaId: kita.id, title: "Sommerfest: Helferliste und Ablauf", content: "## Liebe Familien,\n\nunser Sommerfest findet naechsten Monat im Kita-Garten statt. Bitte merkt euch den Termin vor. Details zu Aufbau, Kuchen und Getraenken folgen ueber das Schwarze Brett.", authorId: admin.id, }, }); await prisma.announcement.create({ data: { kitaId: kita.id, title: "Neue Garderoben-Regelung", content: "Ab Montag bitten wir alle Familien, Wechselkleidung wieder in die beschrifteten Boxen zu legen. So bleibt der Morgen fuer Kinder und Team entspannter.", authorId: admin.id, }, }); await prisma.announcementRead.create({ data: { userId: koordinator.id, announcementId: sommerfest.id, }, }); } 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>, educators: Awaited>, ) { 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 createDutyPlan(context); await createAbsences(context, children); await createInvites(context); await createAnnouncements(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, erzieherUser, koordinator, elternSchmidt, elternYilmaz, pendingParent, }: SeedContext, children: Awaited>, ) { 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(` Erzieherin: ${erzieherUser.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(); });