Files
planer/prisma/demo-seed.ts
Florian WildeandClaude Opus 5 4d0f83ff51 chore: Demo-Daten für die neuen Seiten erweitern
Erfundene Aufgaben mit einer Mischung aus überfällig, heute, später und
ohne Termin, dazu Inbox, Ideen und ein Projekt mit Arbeitspaketen und
einem Meilenstein. Die Fälligkeiten stehen als Versatz zum festen
Demo-Zeitpunkt, damit die Bilder zwischen zwei Läufen gleich bleiben.

Eine Aufgabe trägt eine erfundene Zammad-Herkunft, damit die Darstellung
der Quelle in den Bildern zu sehen ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:33:34 +02:00

356 lines
9.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Daten für die Screenshot-Instanz.
*
* Alles hier ist frei erfunden. Diese Datei darf niemals gegen eine
* Datenbank mit echten Daten laufen — deshalb bricht sie ab, wenn
* DEMO_MODUS nicht ausdrücklich auf true steht.
*
* Aufruf: über npm run screenshots, nicht von Hand.
*/
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import bcrypt from 'bcryptjs'
import { PrismaClient } from '../src/generated/prisma/client.js'
if (process.env.DEMO_MODUS !== 'true') {
console.error(
'Der Demo-Seed läuft nur mit DEMO_MODUS=true. Das ist Absicht: er würde ' +
'sonst erfundene Daten in eine echte Datenbank schreiben.',
)
process.exit(1)
}
const verbindung = process.env.DATABASE_URL
if (!verbindung) {
console.error('DATABASE_URL ist nicht gesetzt.')
process.exit(1)
}
const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: verbindung }) })
export const DEMO_EMAIL = 'martina.beispiel@planer.invalid'
export const DEMO_PASSWORT = 'demo-instanz-nur-fuer-bilder'
const BEREICHE = [
{
name: 'Firma',
farbe: '#2563eb',
reihenfolge: 1,
arbeitsfensterVon: 450,
arbeitsfensterBis: 990,
kapazitaetMo: 360,
kapazitaetDi: 360,
kapazitaetMi: 360,
kapazitaetDo: 360,
kapazitaetFr: 240,
kapazitaetSa: 0,
kapazitaetSo: 0,
},
{
name: 'Gewerbe',
farbe: '#7c3aed',
reihenfolge: 2,
arbeitsfensterVon: 1020,
arbeitsfensterBis: 1260,
kapazitaetMo: 90,
kapazitaetDi: 90,
kapazitaetMi: 90,
kapazitaetDo: 90,
kapazitaetFr: 60,
kapazitaetSa: 180,
kapazitaetSo: 0,
},
{
name: 'Privat',
farbe: '#16a34a',
reihenfolge: 3,
arbeitsfensterVon: 1020,
arbeitsfensterBis: 1320,
kapazitaetMo: 60,
kapazitaetDi: 60,
kapazitaetMi: 60,
kapazitaetDo: 60,
kapazitaetFr: 120,
kapazitaetSa: 240,
kapazitaetSo: 240,
},
{
name: 'Homelab',
farbe: '#ea580c',
reihenfolge: 4,
arbeitsfensterVon: 1140,
arbeitsfensterBis: 1380,
kapazitaetMo: 0,
kapazitaetDi: 60,
kapazitaetMi: 0,
kapazitaetDo: 60,
kapazitaetFr: 0,
kapazitaetSa: 120,
kapazitaetSo: 60,
},
]
/** Erfundene Protokolleinträge, damit die Protokollseite nicht leer wirkt. */
const PROTOKOLL = [
{ aktion: 'bereich.angelegt', objektTyp: 'Bereich', details: { name: 'Homelab' } },
{
aktion: 'bereich.geaendert',
objektTyp: 'Bereich',
details: { vorher: 'Nebenerwerb', nachher: 'Gewerbe' },
},
{
aktion: 'passkey.angelegt',
objektTyp: 'Passkey',
details: { bezeichnung: 'Diensthandy' },
},
]
/**
* Erfundene Aufgaben. Die Fälligkeit steht als Versatz in Tagen zum festen
* Demo-Zeitpunkt, damit die Bilder immer dieselbe Mischung aus überfällig,
* heute und später zeigen.
*/
const AUFGABEN = [
// Überfällig
{
titel: 'Angebot Heizungstausch nachfassen',
bereich: 'Gewerbe',
versatz: -3,
prioritaet: 2,
aufwand: 30,
},
{
titel: 'Rechnung 2026-114 korrigieren',
bereich: 'Gewerbe',
versatz: -1,
prioritaet: 1,
aufwand: 20,
},
// Heute und morgen
{
titel: 'Wochenbericht an die Geschäftsführung',
bereich: 'Firma',
versatz: 0,
prioritaet: 2,
aufwand: 45,
},
{
titel: 'Rückruf Frau Sonnenberg',
bereich: 'Firma',
versatz: 0,
prioritaet: 1,
aufwand: 15,
},
{
titel: 'Zahnarzttermin bestätigen',
bereich: 'Privat',
versatz: 1,
prioritaet: 3,
aufwand: 10,
},
// Später
{
titel: 'Backup-Strategie für den Server überdenken',
bereich: 'Homelab',
versatz: 9,
prioritaet: 4,
aufwand: 120,
},
{
titel: 'Reifen wechseln lassen',
bereich: 'Privat',
versatz: 14,
prioritaet: 3,
aufwand: 60,
},
// Ohne Termin
{
titel: 'Ablage im Keller sortieren',
bereich: 'Privat',
versatz: null,
prioritaet: 5,
aufwand: 180,
},
// Erledigt
{
titel: 'Steuerunterlagen zusammenstellen',
bereich: 'Gewerbe',
versatz: -5,
prioritaet: 3,
aufwand: 90,
status: 'ERLEDIGT' as const,
},
]
/** Liegt noch in der Inbox – ohne Bereich. */
const INBOX = [
{ titel: 'Sicherungskasten beschriften' },
{ titel: 'Angebot Netzwerkschrank einholen' },
]
const IDEEN = [
{ titel: 'Newsletter für Bestandskunden' },
{ titel: 'Werkstatt-Podcast aufnehmen' },
{ titel: 'Regale im Lager neu beschriften' },
]
const PROJEKT_AUFGABEN = [
{
titel: 'Anforderungen sammeln',
typ: 'ARBEITSPAKET' as const,
status: 'ERLEDIGT' as const,
},
{
titel: 'Entwurf abstimmen',
typ: 'ARBEITSPAKET' as const,
status: 'ERLEDIGT' as const,
},
{ titel: 'Texte schreiben', typ: 'ARBEITSPAKET' as const, status: 'OFFEN' as const },
{ titel: 'Fotos beauftragen', typ: 'ARBEITSPAKET' as const, status: 'WARTET' as const },
{ titel: 'Seite ist online', typ: 'MEILENSTEIN' as const, status: 'OFFEN' as const },
]
/** Fester Bezugspunkt, passend zu src/server/zeitgeber.ts. */
const DEMO_JETZT = new Date('2026-03-17T08:20:00.000Z')
function tageVersetzt(tage: number): Date {
return new Date(DEMO_JETZT.getTime() + tage * 24 * 60 * 60 * 1000)
}
async function main(): Promise<void> {
console.log('Demo-Daten einspielen.')
for (const bereich of BEREICHE) {
await db.bereich.upsert({
where: { name: bereich.name },
update: bereich,
create: bereich,
})
}
const benutzer = await db.benutzer.upsert({
where: { email: DEMO_EMAIL },
update: { rolle: 'ADMIN', name: 'Martina Beispiel' },
create: {
email: DEMO_EMAIL,
name: 'Martina Beispiel',
rolle: 'ADMIN',
passwortHash: await bcrypt.hash(DEMO_PASSWORT, 10),
},
})
await db.auditEintrag.deleteMany({})
// Feste Zeitpunkte, damit die Bilder zwischen zwei Läufen gleich bleiben.
let versatz = 0
for (const eintrag of PROTOKOLL) {
versatz += 37
await db.auditEintrag.create({
data: {
zeitpunkt: new Date(Date.UTC(2026, 2, 17, 9, versatz % 60)),
benutzerId: benutzer.id,
benutzerEmail: benutzer.email,
aktion: eintrag.aktion,
objektTyp: eintrag.objektTyp,
details: eintrag.details,
instanz: 'daheim',
},
})
}
await db.passkey.deleteMany({ where: { benutzerId: benutzer.id } })
await db.passkey.create({
data: {
benutzerId: benutzer.id,
credentialId: 'demo-passkey-diensthandy',
publicKey: Buffer.from([0]),
bezeichnung: 'Diensthandy',
gesichert: true,
erstelltAm: new Date(Date.UTC(2026, 1, 3, 8, 15)),
zuletztBenutzt: new Date(Date.UTC(2026, 2, 17, 7, 42)),
},
})
// --- Aufgaben, Ideen und ein Projekt -------------------------------------
await db.aufgabe.deleteMany({})
await db.projekt.deleteMany({})
const bereichsKennung = new Map(
(await db.bereich.findMany({ select: { id: true, name: true } })).map((b) => [
b.name,
b.id,
]),
)
for (const eintrag of AUFGABEN) {
await db.aufgabe.create({
data: {
titel: eintrag.titel,
bereichId: bereichsKennung.get(eintrag.bereich) ?? null,
prioritaet: eintrag.prioritaet,
aufwandMinuten: eintrag.aufwand,
faelligAm: eintrag.versatz === null ? null : tageVersetzt(eintrag.versatz),
status: eintrag.status ?? 'OFFEN',
erledigtAm: eintrag.status === 'ERLEDIGT' ? tageVersetzt(-4) : null,
},
})
}
for (const eintrag of INBOX) {
await db.aufgabe.create({ data: { titel: eintrag.titel } })
}
for (const eintrag of IDEEN) {
await db.aufgabe.create({ data: { titel: eintrag.titel, typ: 'IDEE' } })
}
const projekt = await db.projekt.create({
data: {
name: 'Website neu aufsetzen',
beschreibung:
'Der alte Auftritt ist von 2019 und auf dem Handy kaum lesbar. Fertig ist es, wenn die neue Seite online ist und das Kontaktformular ankommt.',
bereichId: bereichsKennung.get('Gewerbe') ?? null,
zielAm: tageVersetzt(45),
},
})
for (const eintrag of PROJEKT_AUFGABEN) {
await db.aufgabe.create({
data: {
titel: eintrag.titel,
typ: eintrag.typ,
status: eintrag.status,
projektId: projekt.id,
bereichId: bereichsKennung.get('Gewerbe') ?? null,
aufwandMinuten: eintrag.typ === 'MEILENSTEIN' ? null : 120,
erledigtAm: eintrag.status === 'ERLEDIGT' ? tageVersetzt(-7) : null,
},
})
}
// Eine Aufgabe, die von außen kam – zeigt die Herkunft in der Liste.
await db.aufgabe.create({
data: {
titel: 'Störung Klimagerät Serverraum prüfen',
bereichId: bereichsKennung.get('Firma') ?? null,
prioritaet: 2,
aufwandMinuten: 60,
faelligAm: tageVersetzt(2),
quelle: 'ZAMMAD',
quelleRef: 'demo-38421',
quelleUrl: 'https://zammad.example.invalid/ticket/38421',
},
})
console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`)
}
main()
.then(async () => {
await db.$disconnect()
})
.catch(async (fehler: unknown) => {
console.error(fehler)
await db.$disconnect()
process.exit(1)
})