// SPDX-FileCopyrightText: 2026 Paul Schoeneck // // SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms import "dotenv/config"; import { db } from "../src/lib/db"; import { auditContext } from "../src/lib/audit"; import { localClock, localInstant } from "../src/lib/time"; import { getSlots } from "../src/lib/slots"; const demoChildren = [ ["Mila", "Sonnental", "1a"], ["Noah", "Wiesenbach", "2b"], ["Emma", "Blütenfeld", "3a"], ["Finn", "Wolkenstein", "1b"], ["Lina", "Sommerhain", "2a"], ["Ben", "Sternwald", "4a"], ["Hannah", "Lindenau", "3b"], ["Leon", "Regenbogen", "1a"], ["Clara", "Morgenrot", "2b"], ["Elias", "Birkenfels", "4b"], ["Amelie", "Fuchswiese", "3a"], ["Oskar", "Kleebach", "2a"], ]; const clockTime = (minutes: number) => { const clamped = Math.max(1, Math.min(1439, minutes)); return `${Math.floor(clamped / 60).toString().padStart(2, "0")}:${(clamped % 60).toString().padStart(2, "0")}`; }; async function seed() { const staff = await db.query<{ id: string }>("SELECT id FROM staff ORDER BY created_at, id LIMIT 1"); if (!staff.rowCount) throw new Error("Zuerst npm run db:setup ausführen."); const client = await db.connect(); const now = new Date(); const clock = localClock(now); const timeZone = process.env.APP_TIMEZONE ?? "Europe/Berlin"; const slots = (await getSlots()).filter((slot) => slot.active); const currentMinute = Number(clock.time.slice(0, 2)) * 60 + Number(clock.time.slice(3, 5)); let inserted = 0; try { await client.query("BEGIN"); await auditContext(client, staff.rows[0].id, "Fiktive Demodaten zum Testen – keine realen Personen."); for (const [index, [firstName, lastName, className]] of demoChildren.entries()) { const id = `d3000000-0000-4000-8000-${(index + 1).toString().padStart(12, "0")}`; const result = await client.query(`INSERT INTO children(id, first_name, last_name, class_name) VALUES ($1, $2, $3, $4) ON CONFLICT(id) DO NOTHING RETURNING id`, [id, firstName, lastName, className]); if (!result.rowCount) continue; inserted++; const weekdays = [...new Set([1, 2, 3, 4, 5, clock.weekday])]; for (const weekday of weekdays) { const slot = slots.length ? slots[(index + weekday) % slots.length] : undefined; await client.query(`INSERT INTO weekly_schedules(child_id, weekday, arrival, pickup, pickup_end, pickup_slot_id) VALUES ($1, $2, $3, $4, $5, $6)`, [id, weekday, weekday % 2 ? "12:00" : "12:30", slot?.start ?? "16:00", slot?.end ?? null, slot?.id ?? null]); } await client.query(`INSERT INTO pickup_contacts(child_id, name, relationship, authorized, notes) VALUES ($1, $2, 'Mutter', true, 'Fiktiver Demokontakt. Ausweis bei unbekannter Person prüfen.'), ($1, $3, 'Vater', true, 'Fiktiver Demokontakt.')`, [id, `Alex ${lastName}`, `Robin ${lastName}`]); if (index % 3 === 0) { await client.query(`INSERT INTO pickup_contacts(child_id, name, relationship, authorized, notes) VALUES ($1, $2, 'Bekannte Person', false, 'Demobeispiel: keine aktuelle Abholberechtigung.')`, [id, `Kim ${lastName}`]); } if ([2, 5, 9].includes(index)) { for (const weekday of weekdays) { await client.query("INSERT INTO alone_rules(child_id, weekday, earliest) VALUES ($1, $2, $3)", [id, weekday, weekday === clock.weekday ? clockTime(currentMinute - 10) : "15:00"]); } } let historyIndex = 0; for (let offset = 1; offset <= 14; offset++) { const date = new Date(`${clock.day}T12:00:00Z`); date.setUTCDate(date.getUTCDate() - offset); if ([0, 6].includes(date.getUTCDay())) continue; const day = date.toISOString().slice(0, 10); const sick = (index + offset) % 11 === 0; const forgotten = index === 0 && historyIndex === 0; historyIndex++; const arrival = sick ? null : localInstant(day, "12:15", timeZone); const pickup = sick || forgotten ? null : localInstant(day, clockTime(14 * 60 + (index % 5) * 30), timeZone); await client.query(`INSERT INTO attendance(child_id, day, status, arrived_at, picked_up_at, pickup_kind, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [id, day, sick ? "sick" : forgotten ? "arrived" : "picked_up", arrival, pickup, pickup ? index % 4 === 0 ? "parents" : "planned" : null, staff.rows[0].id]); } const plannedPickup = clockTime(currentMinute + (index === 0 ? -10 : index === 1 || index === 2 ? 10 : 120 + index * 10)); const arrivalTime = clockTime(currentMinute - 90); if (index === 8) { await client.query("INSERT INTO day_overrides(child_id, day, attending) VALUES ($1, $2, false)", [id, clock.day]); } else { await client.query(`INSERT INTO day_overrides(child_id, day, attending, arrival, pickup) VALUES ($1, $2, true, $3, $4)`, [id, clock.day, "00:00", plannedPickup]); } if ([7, 8, 10].includes(index) || currentMinute < 3) continue; const sick = index === 4; const picked = index === 5 || index === 6; await client.query(`INSERT INTO attendance(child_id, day, status, arrived_at, picked_up_at, pickup_kind, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [id, clock.day, sick ? "sick" : picked ? "picked_up" : "arrived", sick ? null : localInstant(clock.day, arrivalTime, timeZone), picked ? localInstant(clock.day, clockTime(currentMinute - 5), timeZone) : null, picked ? index === 5 ? "parents" : "planned" : null, staff.rows[0].id]); } await client.query("COMMIT"); console.log(`${inserted} fiktive Kinder angelegt (${demoChildren.length - inserted} bereits vorhanden, unverändert).`); console.log("Mit Wochenplänen, Tagesabweichungen, Abholberechtigten und zwei Wochen Verlauf."); console.log("Mila Sonnental hat im letzten Betreuungstag eine vergessene Abholung zum Korrigieren."); console.log("Passwörter und bestehende Datensätze wurden nicht verändert."); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } seed().catch((error: unknown) => { console.error("Demodaten konnten nicht angelegt werden:", error); process.exitCode = 1; }).finally(() => db.end());