continued the kita-planer

This commit is contained in:
t.indorf
2026-05-08 14:32:14 +02:00
parent b686e714ff
commit 7aff691803
85 changed files with 9434 additions and 588 deletions
+179 -23
View File
@@ -32,6 +32,7 @@ enum UserRole {
SUPERADMIN
ADMIN
KOORDINATOR
ERZIEHER
ELTERN
}
@@ -73,6 +74,18 @@ enum NotdienstAlertStatus {
CANCELLED
}
enum DutyAssignmentStatus {
PLANNED
DONE
CANCELLED
}
enum AbsenceReason {
ILLNESS
VACATION
OTHER
}
// =====================================================================
// TENANT
// =====================================================================
@@ -96,10 +109,15 @@ model Kita {
// Relations (Cascade auf alle Tenant-Daten)
users User[]
families Family[]
children Child[]
educators Educator[]
parentDuties ParentDuty[]
parentDutyAssignments ParentDutyAssignment[]
dutyTypes DutyType[]
dutyAssignments DutyAssignment[]
absences Absence[]
announcements Announcement[]
invitations Invitation[]
termine Termin[]
mitbringselItems MitbringselItem[]
@@ -107,11 +125,33 @@ model Kita {
notdienstAvailabilities NotdienstAvailability[]
notdienstAssignments NotdienstAssignment[]
notdienstAlerts NotdienstAlert[]
childParents ChildParent[]
@@map("kitas")
}
// =====================================================================
// FAMILIES / HAUSHALTE
// =====================================================================
model Family {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
children Child[]
dutyAssignments DutyAssignment[]
@@index([kitaId])
@@map("families")
}
// =====================================================================
// USERS
// =====================================================================
@@ -124,6 +164,11 @@ model User {
kitaId String?
kita Kita? @relation(fields: [kitaId], references: [id], onDelete: Cascade)
/// Optional, weil Admins/Superadmins keinem Haushalt angehören müssen.
/// Eltern-User werden beim Löschen ihrer Familie kaskadiert entfernt.
familyId String?
family Family? @relation(fields: [familyId], references: [id], onDelete: Cascade)
email String @unique
passwordHash String
@@ -157,13 +202,14 @@ model User {
updatedAt DateTime @updatedAt
// Relations
childLinks ChildParent[]
dutyAssignments ParentDutyAssignment[]
notdienstAvailabilities NotdienstAvailability[]
notdienstAlertsAssigned NotdienstAlert[] @relation("NotdienstAlertParent")
notdienstAlertsTriggered NotdienstAlert[] @relation("NotdienstAlertTrigger")
notdienstPlansCreated NotdienstPlan[] @relation("NotdienstPlanCreator")
announcementsAuthored Announcement[] @relation("AnnouncementAuthor")
announcementReads AnnouncementRead[]
termineCreated Termin[] @relation("TerminCreator")
termineApproved Termin[] @relation("TerminApprover")
@@ -172,6 +218,7 @@ model User {
invitationsCreated Invitation[] @relation("InvitationCreator")
@@index([kitaId])
@@index([familyId])
@@index([kitaId, role])
@@map("users")
}
@@ -185,6 +232,9 @@ model Child {
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
familyId String
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
firstName String
lastName String
dateOfBirth DateTime?
@@ -195,34 +245,15 @@ model Child {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parentLinks ChildParent[]
notdienstAvailabilities NotdienstAvailability[]
notdienstAssignments NotdienstAssignment[]
absences Absence[]
@@index([kitaId])
@@index([familyId])
@@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)
// =====================================================================
@@ -285,6 +316,131 @@ model ParentDutyAssignment {
@@map("parent_duty_assignments")
}
// =====================================================================
// DUTY PLANNING (Top-Down-Dienstplan fuer Haushalte)
// =====================================================================
model DutyType {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
name String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments DutyAssignment[]
@@unique([kitaId, name])
@@index([kitaId])
@@map("duty_types")
}
model DutyAssignment {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
familyId String
family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
dutyTypeId String
dutyType DutyType @relation(fields: [dutyTypeId], references: [id], onDelete: Cascade)
startDate DateTime @db.Date
endDate DateTime @db.Date
status DutyAssignmentStatus @default(PLANNED)
reminderSentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([kitaId, dutyTypeId, startDate])
@@index([kitaId, startDate])
@@index([familyId, startDate])
@@map("duty_assignments")
}
// =====================================================================
// ABSENCES (Abwesenheits- und Krankmeldungen)
// =====================================================================
model Absence {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
childId String
child Child @relation(fields: [childId], references: [id], onDelete: Cascade)
startDate DateTime @db.Date
endDate DateTime @db.Date
reason AbsenceReason
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kitaId, startDate, endDate])
@@index([childId, startDate])
@@map("absences")
}
// =====================================================================
// ANNOUNCEMENTS (Digitales Schwarzes Brett)
// =====================================================================
model Announcement {
id String @id @default(cuid())
kitaId String
kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade)
title String
content String
authorId String
author User @relation("AnnouncementAuthor", fields: [authorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
attachments Attachment[]
reads AnnouncementRead[]
@@index([kitaId, createdAt])
@@index([authorId])
@@map("announcements")
}
model Attachment {
id String @id @default(cuid())
announcementId String
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
fileName String
fileUrl String
fileType String
@@index([announcementId])
@@map("attachments")
}
model AnnouncementRead {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
announcementId String
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
readAt DateTime @default(now())
@@unique([userId, announcementId])
@@index([announcementId])
@@map("announcement_reads")
}
// =====================================================================
// INVITATIONS (Invite-Only Onboarding, Modul 3)
// =====================================================================
+182 -22
View File
@@ -1,4 +1,5 @@
import {
AbsenceReason,
InvitationStatus,
NotdienstAlertStatus,
NotdienstPlanStatus,
@@ -29,6 +30,7 @@ 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",
@@ -52,6 +54,16 @@ function addDays(date: Date, days: number) {
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);
}
@@ -156,6 +168,27 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
},
});
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,
@@ -175,9 +208,28 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
},
});
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",
@@ -197,6 +249,7 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
const elternSchmidt = await prisma.user.create({
data: {
kitaId: kita.id,
familyId: familySchmidtYilmaz.id,
email: "schmidt@waldameisen.local",
passwordHash,
firstName: "Lukas",
@@ -211,6 +264,7 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
const elternYilmaz = await prisma.user.create({
data: {
kitaId: kita.id,
familyId: familySchmidtYilmaz.id,
email: "yilmaz@waldameisen.local",
passwordHash,
firstName: "Aylin",
@@ -230,6 +284,7 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
const pendingParent = await prisma.user.create({
data: {
kitaId: kita.id,
familyId: familyFischer.id,
email: "pending@waldameisen.local",
passwordHash: "",
firstName: "Lena",
@@ -240,8 +295,12 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
return {
kita,
familyMueller,
familySchmidtYilmaz,
familyFischer,
superAdmin,
admin,
erzieherUser,
koordinator,
elternSchmidt,
elternYilmaz,
@@ -251,72 +310,58 @@ async function createDemoUsers(passwordHash: string, consentAt: Date) {
async function createChildren({
kita,
koordinator,
elternSchmidt,
elternYilmaz,
pendingParent,
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"),
parentLinks: {
create: { kitaId: kita.id, userId: koordinator.id },
},
},
});
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.",
parentLinks: {
create: { kitaId: kita.id, userId: koordinator.id },
},
},
});
const clara = await prisma.child.create({
data: {
kitaId: kita.id,
familyId: familySchmidtYilmaz.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,
familyId: familySchmidtYilmaz.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,
familyId: familyFischer.id,
firstName: "Nina",
lastName: "Fischer",
dateOfBirth: new Date("2022-05-30"),
parentLinks: {
create: { kitaId: kita.id, userId: pendingParent.id },
},
},
});
@@ -384,6 +429,83 @@ async function createParentDuties({
});
}
async function createAbsences(
{ kita }: SeedContext,
children: Awaited<ReturnType<typeof createChildren>>,
) {
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);
@@ -408,6 +530,39 @@ async function createInvites({ kita, admin, pendingParent }: SeedContext) {
});
}
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,
@@ -601,7 +756,10 @@ async function createDemoData() {
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);
@@ -622,6 +780,7 @@ function printSummary(
kita,
superAdmin,
admin,
erzieherUser,
koordinator,
elternSchmidt,
elternYilmaz,
@@ -636,6 +795,7 @@ function printSummary(
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}`);