feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig. Ersteinrichtung in drei Schritten, bewusst ausserhalb des Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert. Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf. Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich sonst dauerhaft aussperren. Erst danach gibt es die Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash. Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts gerechnet statt ueber eine Differenz, damit Schaltjahre und der 29. Februar von selbst stimmen. Daran haengen Jahresbericht, Rechnungslegung und die Ausschlussfrist der Aufwandspauschale. Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch dann speicherbar bleiben, wenn das Fristenmodul fehlt. Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu. Die Aenderungen liegen verschluesselt, weil dort Diagnosen und Kontostaende stehen koennen. Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang. Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026, leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk mit acht Nachforderungen. Nur ausserhalb der Produktion. Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1. Etappe: 1 Phase: Fundament
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 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, Algorithm } from '@node-rs/argon2'
|
||||
import { createCipheriv, randomBytes } from 'node:crypto'
|
||||
|
||||
const db = new PrismaClient()
|
||||
|
||||
const ARGON2 = {
|
||||
algorithm: Algorithm.Argon2id,
|
||||
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())
|
||||
Reference in New Issue
Block a user