Files
betreuungs-cockpit/prisma/seed.ts
T
Florian Wilde 1234a57fa6 fix: Typisierung von oeffnen(), CSP im Entwicklungsmodus, Nonce-Abweichung
Drei Fehler, die sich beim Durchsehen der Typpruefung und der
Browserkonsole gezeigt haben.

1. oeffnen() lieferte Record<string, string | null>. Zusammen mit
   noUncheckedIndexedAccess wurde daraus bei jedem Zugriff
   string | null | undefined - und jede aufrufende Seite haette das
   wegkaesen muessen, obwohl die Felder immer da sind. Jetzt leitet ein
   Mapped Type die Klartextnamen aus VERSCHLUESSELTE_FELDER ab:
   oeffnen('Betreute', zeile).diagnosen ist sauber string | null.
   Das hat auf einen Schlag 17 Folgefehler in fremden Modulen erledigt.

2. Die CSP blockierte im Entwicklungsmodus die eigenen Skripte.
   'strict-dynamic' schaltet die Freigabe nach Herkunft ab, also auch
   'self' - und Hot-Reload und Fehleranzeige laden ohne Nonce. Man sah
   eine tote Seite statt der Fehlermeldung. Im Produktivbetrieb bleibt
   es bei Nonce plus 'strict-dynamic', im Entwicklungsmodus gilt die
   lockere Fassung.

3. Das Theme-Skript im Layout meldete bei jedem Seitenaufruf eine
   Hydrations-Abweichung: React gibt das nonce-Attribut absichtlich
   nicht an den Browser weiter, serverseitig steht es aber im HTML.
   suppressHydrationWarning am Skript selbst - das html-Element allein
   deckt das nicht ab.

Nebenbei: Algorithm.Argon2id ist ein const enum und laesst sich mit
isolatedModules nicht importieren. Die Angabe entfaellt, @node-rs/argon2
benutzt von sich aus Argon2id.

Die Anmeldeseite laedt jetzt ohne einen einzigen Konsolenfehler.

Etappe: 8
Phase: Haertung
2026-08-22 06:06:20 +02:00

547 lines
22 KiB
TypeScript

/**
* Startdaten.
*
* npm run db:seed
*
* Der Seed legt bewusst KEINE echten Personendaten an. Er richtet nur den
* ersten Zugang ein; alles Weitere fragt der Ersteinrichtungs-Assistent
* beim ersten Start ab. So landen keine Gesundheitsdaten im Repository,
* und der Seed bleibt auch dann brauchbar, wenn eine zweite Betreuung
* dazukommt.
*
* Zwei Schalter fuer die Entwicklung:
*
* SEED_DEMO=true legt einen vollstaendigen Beispielfall an
* (Erika Musterfrau). Nur ausserhalb der
* Produktion, und in der Oberflaeche als
* Demo gekennzeichnet.
* SEED_DEV_TOTP_SECRET=... aktiviert den zweiten Faktor mit einem
* bekannten Geheimnis, damit automatisierte
* Tests sich anmelden koennen. Wird in der
* Produktion ignoriert.
*
* Der Seed ist idempotent: mehrfaches Ausfuehren aendert nichts.
*/
import { PrismaClient } from '@prisma/client'
import { hash } from '@node-rs/argon2'
import { createCipheriv, randomBytes } from 'node:crypto'
const db = new PrismaClient()
// Muss zu ARGON2_OPTIONEN in src/lib/crypto/hash.ts passen.
// Ohne Angabe benutzt @node-rs/argon2 von sich aus Argon2id.
const ARGON2 = {
memoryCost: 19_456,
timeCost: 2,
parallelism: 1,
} as const
const istProduktion = process.env.NODE_ENV === 'production'
/**
* Verschluesselt wie src/lib/crypto/feld.ts. Bewusst hier noch einmal
* ausgeschrieben: der Seed laeuft ausserhalb von Next.js und kann das
* Modul mit seinem "server-only"-Import nicht laden.
*/
function verschluesseln(klartext: string | null, kontext: string): string | null {
if (!klartext) return null
const version = Number.parseInt(process.env.ENC_KEY_AKTIV ?? '1', 10)
const roh = process.env[`ENC_KEY_V${version}`]
if (!roh) throw new Error(`ENC_KEY_V${version} fehlt. Erzeuge ihn mit "npm run schluessel".`)
const schluessel = Buffer.from(roh, 'base64')
if (schluessel.length !== 32) throw new Error(`ENC_KEY_V${version} muss 32 Bytes lang sein.`)
const iv = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', schluessel, iv, { authTagLength: 16 })
cipher.setAAD(Buffer.from(`bc1:${version}:${kontext}`, 'utf8'))
const geheim = Buffer.concat([cipher.update(klartext, 'utf8'), cipher.final()])
return [
'bc1',
version,
iv.toString('base64url'),
cipher.getAuthTag().toString('base64url'),
geheim.toString('base64url'),
].join('.')
}
function datum(text: string): Date {
return new Date(`${text}T00:00:00.000Z`)
}
function cent(euro: number): bigint {
return BigInt(Math.round(euro * 100))
}
async function ersterZugang() {
const email = (process.env.SEED_ADMIN_EMAIL ?? '').trim().toLowerCase()
const passwort = process.env.SEED_ADMIN_PASSWORT
if (!email || !passwort) {
console.log(
'Kein SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORT gesetzt - der erste Zugang wird beim ' +
'Aufruf von /ersteinrichtung im Browser angelegt.',
)
return null
}
const vorhanden = await db.user.findUnique({ where: { email } })
if (vorhanden) {
console.log(`Zugang ${email} besteht bereits.`)
return vorhanden
}
// Der zweite Faktor bleibt normalerweise offen und wird beim ersten
// Anmelden eingerichtet - nur die Entwicklung darf ihn vorbelegen.
const devSecret = !istProduktion ? process.env.SEED_DEV_TOTP_SECRET : undefined
const benutzer = await db.user.create({
data: {
email,
name: process.env.SEED_ADMIN_NAME ?? 'Betreuer',
passwortHash: await hash(passwort, ARGON2),
rolle: 'ADMIN',
...(devSecret
? {
totpSecretVerschluesselt: verschluesseln(devSecret, 'totpSecretVerschluesselt'),
totpAktiviertAm: new Date(),
}
: {}),
},
})
console.log(`Zugang ${email} angelegt.`)
if (devSecret) {
console.log(' Zweiter Faktor mit dem Entwicklungs-Geheimnis vorbelegt.')
} else {
console.log(' Der zweite Faktor wird beim ersten Anmelden eingerichtet.')
}
return benutzer
}
/**
* Vollstaendiger Beispielfall. Nur mit SEED_DEMO=true und nie in der
* Produktion - er soll zeigen, wie die Anwendung mit Inhalt aussieht.
*/
async function demofall(userId: string) {
const vorhanden = await db.betreute.findFirst({
where: { nachname: 'Musterfrau', vorname: 'Erika' },
})
if (vorhanden) {
console.log('Demofall besteht bereits.')
return
}
const betreute = await db.betreute.create({
data: {
anrede: 'Frau',
vorname: 'Erika',
nachname: 'Musterfrau',
geburtsname: 'Beispiel',
geburtsdatum: datum('1943-04-17'),
geburtsort: 'Bayreuth',
heimName: 'Seniorenzentrum St. Katharina',
heimStrasse: 'Kirchweg 12',
heimPlz: '95444',
heimOrt: 'Bayreuth',
heimZimmer: '218',
heimTelefon: '0921 123456-218',
heimEinzugAm: datum('2026-01-19'),
fruehereStrasse: 'Am Sonnenhang 4',
frueherePlz: '95463',
fruehererOrt: 'Bindlach',
krankenkasse: 'AOK Bayern',
krankenversichertennummerVerschluesselt: verschluesseln(
'A123456789',
'krankenversichertennummerVerschluesselt',
),
pflegekasse: 'AOK Pflegekasse Bayern',
pflegegrad: 3,
pflegegradSeit: datum('2025-11-01'),
pflegegradBescheidVom: datum('2025-10-22'),
schwerbehinderungGdB: 80,
schwerbehinderungMerkzeichen: 'G, B',
diagnosenVerschluesselt: verschluesseln(
[
'COPD GOLD III mit chronischer respiratorischer Insuffizienz (J44.09)',
'Langzeitsauerstofftherapie seit 03/2025, 2 l/min, 16 h/Tag',
'Arterielle Hypertonie (I10)',
'Osteoporose mit Wirbelkörperfraktur BWK 12 (M80.08)',
'Beginnende vaskuläre Demenz (F01.9)',
].join('\n'),
'diagnosenVerschluesselt',
),
ermittelterWilleVerschluesselt: verschluesseln(
'Möchte im Seniorenzentrum bleiben, dort ist eine frühere Nachbarin. ' +
'Will das Haus in Bindlach "nicht einfach hergeben", versteht aber, dass die ' +
'Pflegekosten gedeckt werden müssen. Wünscht keine künstliche Beatmung und keine ' +
'Verlegung in eine Klinik, wenn es zum Ende geht — so auch in der Patientenverfügung ' +
'vom 08.03.2019 festgehalten. Besuche bitte nachmittags, vormittags ist sie erschöpft.',
'ermittelterWilleVerschluesselt',
),
patientenverfuegungVorhanden: true,
vorsorgevollmachtVorhanden: false,
betreuungsgericht: 'Amtsgericht Bayreuth',
aktenzeichenGericht: 'XVII 412/25',
bestellungAm: datum('2026-02-14'),
aufgabenkreise: [
'VERMOEGENSSORGE',
'GESUNDHEITSSORGE',
'AUFENTHALTSBESTIMMUNG',
'WOHNUNGSANGELEGENHEITEN',
'BEHOERDENANGELEGENHEITEN',
],
befreiterBetreuer: true,
ehrenamtlich: true,
aktenzeichenBezirk: 'HzP 2026/04711',
aktenzeichenSozialamt: 'SA-4711-26',
rentenversicherungsnummerVerschluesselt: verschluesseln(
'17 170443 M 512',
'rentenversicherungsnummerVerschluesselt',
),
notizenVerschluesselt: verschluesseln(
'Dies ist ein Demofall zum Ausprobieren. Er lässt sich unter ' +
'Einstellungen jederzeit vollständig löschen.',
'notizenVerschluesselt',
),
zugaenge: { create: { userId, rolle: 'BETREUER' } },
},
})
// --- Kontakte -----------------------------------------------------------
await db.kontakt.createMany({
data: [
{
betreuteId: betreute.id,
name: 'Frau Hartmann',
organisation: 'Amtsgericht Bayreuth',
rolle: 'RECHTSPFLEGER',
telefon: '0921 504-0',
aktenzeichen: 'XVII 412/25',
},
{
betreuteId: betreute.id,
name: 'Herr Doblinger',
organisation: 'Bezirk Oberfranken, Sozialverwaltung',
rolle: 'SACHBEARBEITER_BEZIRK',
telefon: '0921 604-1234',
email: 'sozialverwaltung@bezirk-oberfranken.de',
aktenzeichen: 'HzP 2026/04711',
},
{
betreuteId: betreute.id,
name: 'Frau Kellner',
organisation: 'Seniorenzentrum St. Katharina',
rolle: 'SOZIALDIENST_HEIM',
telefon: '0921 123456-30',
},
{
betreuteId: betreute.id,
name: 'Dr. med. Reuther',
organisation: 'Hausarztpraxis Bindlach',
rolle: 'HAUSARZT',
telefon: '09208 4477',
},
],
})
// --- Konten und Vermoegen ----------------------------------------------
const giro = await db.konto.create({
data: {
betreuteId: betreute.id,
bezeichnung: 'Girokonto',
bank: 'Sparkasse Bayreuth',
art: 'GIRO',
ibanVerschluesselt: verschluesseln('DE02773501100123456789', 'ibanVerschluesselt'),
saldostaende: {
create: [
{ stichtag: datum('2026-02-14'), betragCent: cent(4218.36), quelle: 'Kontoauszug 02/2026' },
{ stichtag: datum('2026-07-31'), betragCent: cent(2967.11), quelle: 'Kontoauszug 07/2026' },
],
},
},
})
await db.konto.create({
data: {
betreuteId: betreute.id,
bezeichnung: 'Verwahrgeld im Heim',
bank: 'Seniorenzentrum St. Katharina',
art: 'VERWAHRGELD_HEIM',
saldostaende: {
create: { stichtag: datum('2026-07-31'), betragCent: cent(184.2), quelle: 'Abrechnung Heim' },
},
},
})
const immobilie = await db.immobilie.create({
data: {
betreuteId: betreute.id,
bezeichnung: 'Einfamilienhaus Bindlach',
strasse: 'Am Sonnenhang 4',
plz: '95463',
ort: 'Bindlach',
grundbuchamt: 'Amtsgericht Bayreuth',
grundbuchBlatt: '3241',
gemarkung: 'Bindlach',
flurstueck: '812/3',
eigentumsanteil: '1/1',
wohnflaecheQm: 128,
grundstueckQm: 640,
baujahr: 1972,
bewohntStatus: 'leerstehend',
leerstandSeit: datum('2026-01-19'),
statusSozialhilfe: 'UNGEKLAERT',
statusBegruendungVerschluesselt: verschluesseln(
'Nicht mehr selbst bewohnt, seit dem Heimeinzug leerstehend. Schutz nach ' +
'§ 90 Abs. 2 Nr. 8 SGB XII greift daher voraussichtlich nicht. Zu prüfen: ' +
'Härtefall nach § 90 Abs. 3 SGB XII und Darlehen nach § 91 SGB XII statt ' +
'sofortiger Verwertung. Mit dem Bezirk noch nicht abschließend geklärt.',
'statusBegruendungVerschluesselt',
),
belastungen: {
create: {
art: 'Grundschuld',
abteilung: 'III',
laufendeNr: '2',
glaeubiger: 'Sparkasse Bayreuth',
betragCent: cent(30000),
bemerkung: 'Valutiert nach Auskunft der Bank nicht mehr, Löschungsbewilligung angefordert.',
},
},
},
})
const lebensversicherung = await db.vertrag.create({
data: {
betreuteId: betreute.id,
bezeichnung: 'Kapitallebensversicherung',
gesellschaft: 'Musterland Leben AG',
art: 'LEBENSVERSICHERUNG',
status: 'BEITRAGSFREI',
vertragsnummerVerschluesselt: verschluesseln('LV-8842119', 'vertragsnummerVerschluesselt'),
beginnAm: datum('1998-07-01'),
endeAm: datum('2028-07-01'),
bewertungSozialhilfe: 'STRITTIG',
bewertungBegruendungVerschluesselt: verschluesseln(
'Rückkaufswert liegt über dem Schonvermögen. Verwertung wäre grundsätzlich zumutbar, ' +
'aber der Vertrag läuft 2028 aus — Kündigung jetzt bedeutet spürbaren Verlust. ' +
'Härtefall nach § 90 Abs. 3 SGB XII geltend gemacht, Antwort des Bezirks steht aus.',
'bewertungBegruendungVerschluesselt',
),
rueckkaufswertAngefragtAm: datum('2026-03-02'),
},
})
await db.vertrag.create({
data: {
betreuteId: betreute.id,
bezeichnung: 'Sterbegeldversicherung',
gesellschaft: 'Nordheim Sterbekasse VVaG',
art: 'STERBEGELDVERSICHERUNG',
status: 'AKTIV',
vertragsnummerVerschluesselt: verschluesseln('SG-220417', 'vertragsnummerVerschluesselt'),
beitragCent: cent(18.4),
rhythmus: 'MONATLICH',
bewertungSozialhilfe: 'GESCHUETZT',
bewertungBegruendungVerschluesselt: verschluesseln(
'Angemessene Bestattungsvorsorge. Nach § 90 Abs. 3 SGB XII und ständiger ' +
'Rechtsprechung regelmäßig geschützt, weil sie der Würde der Bestattung dient.',
'bewertungBegruendungVerschluesselt',
),
},
})
await db.vermoegensposten.createMany({
data: [
{ betreuteId: betreute.id, bezeichnung: 'Girokonto Sparkasse', art: 'KONTO', kontoId: giro.id, bewertungSozialhilfe: 'VERWERTBAR' },
{ betreuteId: betreute.id, bezeichnung: 'Einfamilienhaus Bindlach', art: 'IMMOBILIE', immobilieId: immobilie.id, bewertungSozialhilfe: 'STRITTIG' },
{ betreuteId: betreute.id, bezeichnung: 'Kapitallebensversicherung', art: 'VERSICHERUNG', vertragId: lebensversicherung.id, bewertungSozialhilfe: 'STRITTIG' },
],
})
const posten = await db.vermoegensposten.findMany({ where: { betreuteId: betreute.id } })
for (const p of posten) {
const werte: Record<string, { wert: number; quelle: string }> = {
IMMOBILIE: { wert: 268000, quelle: 'Bodenrichtwert und Vergleichspreise, eigene Schätzung' },
VERSICHERUNG: { wert: 14820.55, quelle: 'Rückkaufswertmitteilung vom 26.03.2026' },
KONTO: { wert: 2967.11, quelle: 'Kontoauszug 07/2026' },
}
const w = werte[p.art]
if (w) {
await db.wertstand.create({
data: {
vermoegenspostenId: p.id,
stichtag: datum('2026-07-31'),
wertCent: cent(w.wert),
wertquelle: w.quelle,
},
})
}
}
// --- Sozialhilfeantrag mit Nachforderungen ------------------------------
const antrag = await db.sozialhilfeAntrag.create({
data: {
betreuteId: betreute.id,
traeger: 'Bezirk Oberfranken',
leistungsart: 'Hilfe zur Pflege nach dem 7. Kapitel SGB XII',
aktenzeichen: 'HzP 2026/04711',
sachbearbeiter: 'Herr Doblinger',
telefon: '0921 604-1234',
antragAm: datum('2026-02-26'),
eingangBeimTraegerAm: datum('2026-03-02'),
},
})
await db.nachforderung.createMany({
data: [
{ antragId: antrag.id, bezeichnung: 'Rentenbescheide aller Träger', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 1 },
{ antragId: antrag.id, bezeichnung: 'Kontoauszüge der letzten 3 Monate, lückenlos', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 2 },
{ antragId: antrag.id, bezeichnung: 'Pflegegradbescheid', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 3 },
{ antragId: antrag.id, bezeichnung: 'Heimvertrag', status: 'UEBERSANDT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-04-08'), ausCheckliste: true, reihenfolge: 4 },
{ antragId: antrag.id, bezeichnung: 'Aktueller Grundbuchauszug', status: 'UEBERSANDT', angefordertAm: datum('2026-04-21'), uebersandtAm: datum('2026-05-06'), ausCheckliste: true, reihenfolge: 5 },
{ antragId: antrag.id, bezeichnung: 'Rückkaufswert Kapitallebensversicherung zum Antragsdatum', status: 'IN_BESCHAFFUNG', angefordertAm: datum('2026-04-21'), fristBis: datum('2026-09-05'), erlaeuterung: 'Zweite Anfrage an die Gesellschaft am 12.08.2026 raus.', ausCheckliste: true, reihenfolge: 6 },
{ antragId: antrag.id, bezeichnung: 'Notarielle Verträge früherer Grundstücksübertragungen', status: 'OFFEN', angefordertAm: datum('2026-04-21'), fristBis: datum('2026-09-05'), erlaeuterung: 'Der Träger prüft eine Rückforderung wegen Verarmung (§ 528 BGB). Notariat Bayreuth angefragt.', ausCheckliste: true, reihenfolge: 7 },
{ antragId: antrag.id, bezeichnung: 'Erklärung zu Unterhaltspflichtigen', status: 'OFFEN', angefordertAm: datum('2026-07-30'), fristBis: datum('2026-08-31'), ausCheckliste: true, reihenfolge: 8 },
],
})
// --- Vorgaenge ----------------------------------------------------------
await db.vorgang.create({
data: {
betreuteId: betreute.id,
titel: 'Antrag Hilfe zur Pflege beim Bezirk Oberfranken',
kategorie: 'SOZIALHILFE',
status: 'WARTET_AUF_DRITTE',
gegenstelle: 'Bezirk Oberfranken',
aktenzeichen: 'HzP 2026/04711',
naechsterSchritt: 'Rückkaufswert und Übertragungsverträge nachreichen, dann Sachstand erfragen',
letzterKontaktAm: datum('2026-07-30'),
angelegtVonId: userId,
beschreibungVerschluesselt: verschluesseln(
'Antrag vom 26.02.2026. Der Bezirk hat in zwei Wellen nachgefordert. ' +
'Offen sind noch der Rückkaufswert der Lebensversicherung und die notariellen ' +
'Übertragungsverträge aus den 1990er-Jahren.',
'beschreibungVerschluesselt',
),
},
})
await db.vorgang.create({
data: {
betreuteId: betreute.id,
titel: 'Haus Bindlach: Verwertung oder Darlehen klären',
kategorie: 'IMMOBILIE',
status: 'IN_ARBEIT',
gegenstelle: 'Bezirk Oberfranken',
genehmigungspflichtig: true,
naechsterSchritt: 'Verkehrswert belastbar ermitteln lassen, dann Gespräch mit dem Bezirk',
angelegtVonId: userId,
beschreibungVerschluesselt: verschluesseln(
'Das Haus ist seit dem Heimeinzug leerstehend. Ein Verkauf wäre nach § 1850 BGB ' +
'genehmigungspflichtig. Vorrangig prüfen: Darlehen nach § 91 SGB XII gegen ' +
'Grundschuld, damit nicht unter Zeitdruck verkauft werden muss.',
'beschreibungVerschluesselt',
),
},
})
await db.vorgang.create({
data: {
betreuteId: betreute.id,
titel: 'Aufwandspauschale für das erste Betreuungsjahr beantragen',
kategorie: 'GERICHT',
status: 'OFFEN',
gegenstelle: 'Amtsgericht Bayreuth',
aktenzeichen: 'XVII 412/25',
naechsterSchritt: 'Antrag nach Ablauf des Betreuungsjahres am 13.02.2027 stellen',
angelegtVonId: userId,
},
})
// --- Kontaktprotokolle --------------------------------------------------
const besuche: { tag: string; dauer: number; inhalt: string; wunsch?: string }[] = [
{ tag: '2026-06-14', dauer: 75, inhalt: 'Besuch im Heim. Gemeinsam im Garten gesessen. Atmung an diesem Tag ruhig, Sauerstoff 2 l/min. Über das Haus gesprochen.', wunsch: 'Möchte das Haus nicht verkaufen, solange es nicht sein muss.' },
{ tag: '2026-07-05', dauer: 60, inhalt: 'Besuch. Erschöpft, blieb im Zimmer. Pflegedienstleitung berichtet von zwei kurzen Luftnotepisoden in der Vorwoche.' },
{ tag: '2026-07-22', dauer: 30, inhalt: 'Telefonat mit dem Sozialdienst wegen der Heimabrechnung Juli.' },
{ tag: '2026-08-09', dauer: 90, inhalt: 'Besuch mit Enkelin. Deutlich wacher als beim letzten Mal. Wünscht sich häufiger Besuch am Nachmittag.', wunsch: 'Besuche bitte nachmittags, vormittags ist sie zu müde.' },
]
for (const b of besuche) {
await db.kontaktprotokoll.create({
data: {
betreuteId: betreute.id,
datum: new Date(`${b.tag}T15:00:00.000Z`),
art: b.dauer === 30 ? 'TELEFONAT' : 'BESUCH',
dauerMinuten: b.dauer,
mitBetreuter: b.dauer !== 30,
inhaltVerschluesselt: verschluesseln(b.inhalt, 'inhaltVerschluesselt'),
wuenscheVerschluesselt: b.wunsch ? verschluesseln(b.wunsch, 'wuenscheVerschluesselt') : null,
},
})
}
// --- Gesundheitsverlauf -------------------------------------------------
await db.gesundheitsereignis.createMany({
data: [
{ betreuteId: betreute.id, datum: datum('2026-03-11'), art: 'EXAZERBATION', titel: 'Infektexazerbation, ambulant behandelt', behandler: 'Dr. Reuther', beschreibungVerschluesselt: verschluesseln('Antibiotikum und kurzzeitig Prednisolon. Keine Einweisung nötig.', 'beschreibungVerschluesselt') },
{ betreuteId: betreute.id, datum: datum('2026-05-02'), bisDatum: datum('2026-05-09'), art: 'KRANKENHAUS', titel: 'Stationär wegen Luftnot', einrichtung: 'Klinikum Bayreuth', abwesenheitstage: 7, beschreibungVerschluesselt: verschluesseln('Aufnahme über die Notaufnahme. Sauerstoffbedarf danach von 1,5 auf 2 l/min erhöht.', 'beschreibungVerschluesselt') },
{ betreuteId: betreute.id, datum: datum('2026-06-20'), art: 'HILFSMITTEL', titel: 'Mobiler Sauerstoffkonzentrator bewilligt', beschreibungVerschluesselt: verschluesseln('Ermöglicht Ausflüge aus dem Zimmer.', 'beschreibungVerschluesselt') },
{ betreuteId: betreute.id, datum: datum('2026-07-15'), art: 'IMPFUNG', titel: 'Pneumokokken-Impfung', behandler: 'Dr. Reuther' },
],
})
// --- Heimkosten ---------------------------------------------------------
for (const monat of [5, 6, 7]) {
await db.heimkostenmonat.create({
data: {
betreuteId: betreute.id,
jahr: 2026,
monat,
pflegeEigenanteilCent: cent(1489.0),
unterkunftVerpflegungCent: cent(878.4),
investitionskostenCent: cent(486.3),
ausbildungsumlageCent: cent(112.5),
pflegekasseCent: cent(1363.0),
leistungszuschlagProzent: 15,
leistungszuschlagCent: cent(223.35),
renteCent: cent(1284.62),
barbetragCent: cent(135.66),
bekleidungspauschaleCent: cent(27.13),
abwesenheitstage: monat === 5 ? 7 : 0,
},
})
}
console.log('Demofall "Erika Musterfrau" angelegt.')
}
async function main() {
const benutzer = await ersterZugang()
if (process.env.SEED_DEMO === 'true' && !istProduktion && benutzer) {
await demofall(benutzer.id)
} else if (process.env.SEED_DEMO === 'true' && istProduktion) {
console.log('SEED_DEMO wird in der Produktion nicht ausgefuehrt.')
}
console.log('\nFertig. Weiter geht es im Browser unter /ersteinrichtung.')
}
main()
.catch((fehler) => {
console.error('Seed fehlgeschlagen:', fehler)
process.exit(1)
})
.finally(() => db.$disconnect())