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:
@@ -0,0 +1,109 @@
|
||||
import type { GesundheitsArt } from '@prisma/client'
|
||||
import { tag, tageZwischen, tagesdatum } from '@/lib/fristen/datum'
|
||||
|
||||
/**
|
||||
* Abwesenheitstage.
|
||||
*
|
||||
* Warum das gezaehlt wird: Liegt die betreute Person im Krankenhaus, wird
|
||||
* der Heimplatz freigehalten, aber nicht voll berechnet. Ab einer im
|
||||
* Heimvertrag geregelten Zahl von Tagen mindert sich der Eigenanteil.
|
||||
* Die Einrichtung zieht das nicht immer von selbst ab, und der
|
||||
* Sozialhilfetraeger rechnet es erst recht nicht nach - wer die Tage nicht
|
||||
* mitzaehlt, zahlt zu viel.
|
||||
*
|
||||
* Gezaehlt werden die Tage zwischen Aufnahme und Entlassung; der
|
||||
* Entlassungstag zaehlt nicht mit, weil die betreute Person an diesem Tag
|
||||
* wieder im Heim verpflegt wird. Ob der Aufnahmetag mitzaehlt, regelt der
|
||||
* Heimvertrag unterschiedlich - deshalb laesst sich die Zahl an jedem
|
||||
* Ereignis von Hand ueberschreiben.
|
||||
*/
|
||||
|
||||
export type AbwesenheitsEreignis = {
|
||||
datum: Date
|
||||
bisDatum: Date | null
|
||||
abwesenheitstage: number | null
|
||||
art: GesundheitsArt
|
||||
}
|
||||
|
||||
export type Abwesenheitsjahr = {
|
||||
jahr: number
|
||||
tage: number
|
||||
/** Wie viele Ereignisse haben zu diesen Tagen beigetragen? */
|
||||
ereignisse: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Tage eines einzelnen Ereignisses. Ist die Zahl von Hand eingetragen,
|
||||
* gilt sie; sonst wird aus dem Zeitraum gerechnet.
|
||||
*/
|
||||
export function tageEines(ereignis: AbwesenheitsEreignis): number {
|
||||
if (ereignis.abwesenheitstage !== null && ereignis.abwesenheitstage >= 0) {
|
||||
return ereignis.abwesenheitstage
|
||||
}
|
||||
if (!ereignis.bisDatum) return 0
|
||||
return Math.max(0, tageZwischen(ereignis.datum, ereignis.bisDatum))
|
||||
}
|
||||
|
||||
/**
|
||||
* Summe je Kalenderjahr.
|
||||
*
|
||||
* Ein Aufenthalt ueber den Jahreswechsel wird auf beide Jahre aufgeteilt -
|
||||
* die Heimrechnung kommt monatlich, und der Jahresbericht braucht die Tage
|
||||
* dort, wo sie angefallen sind. Nur wenn die Tageszahl von Hand
|
||||
* eingetragen wurde, wird sie ungeteilt dem Jahr des Beginns zugeordnet:
|
||||
* Dann hat jemand bewusst eine Zahl gesetzt, und die soll nicht durch eine
|
||||
* Automatik zerlegt werden.
|
||||
*/
|
||||
export function abwesenheitstageJeJahr(
|
||||
ereignisse: AbwesenheitsEreignis[],
|
||||
): Abwesenheitsjahr[] {
|
||||
const jahre = new Map<number, Abwesenheitsjahr>()
|
||||
|
||||
const dazu = (jahr: number, tage: number) => {
|
||||
if (tage <= 0) return
|
||||
const vorhanden = jahre.get(jahr)
|
||||
if (vorhanden) {
|
||||
vorhanden.tage += tage
|
||||
} else {
|
||||
jahre.set(jahr, { jahr, tage, ereignisse: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
for (const ereignis of ereignisse) {
|
||||
const beginn = tagesdatum(ereignis.datum)
|
||||
const handgezaehlt = ereignis.abwesenheitstage !== null && ereignis.abwesenheitstage >= 0
|
||||
|
||||
if (handgezaehlt) {
|
||||
dazu(beginn.getUTCFullYear(), ereignis.abwesenheitstage!)
|
||||
zaehleEreignis(jahre, beginn.getUTCFullYear())
|
||||
continue
|
||||
}
|
||||
|
||||
if (!ereignis.bisDatum) continue
|
||||
const ende = tagesdatum(ereignis.bisDatum)
|
||||
if (ende.getTime() <= beginn.getTime()) continue
|
||||
|
||||
// Jahresweise zerlegen.
|
||||
let abschnittVon = beginn
|
||||
while (abschnittVon.getTime() < ende.getTime()) {
|
||||
const jahr = abschnittVon.getUTCFullYear()
|
||||
const jahresende = tag(jahr + 1, 1, 1)
|
||||
const abschnittBis = jahresende.getTime() < ende.getTime() ? jahresende : ende
|
||||
dazu(jahr, tageZwischen(abschnittVon, abschnittBis))
|
||||
zaehleEreignis(jahre, jahr)
|
||||
abschnittVon = abschnittBis
|
||||
}
|
||||
}
|
||||
|
||||
return [...jahre.values()].sort((a, b) => b.jahr - a.jahr)
|
||||
}
|
||||
|
||||
function zaehleEreignis(jahre: Map<number, Abwesenheitsjahr>, jahr: number): void {
|
||||
const eintrag = jahre.get(jahr)
|
||||
if (eintrag) eintrag.ereignisse += 1
|
||||
}
|
||||
|
||||
/** Gesamtsumme ueber alle Jahre - fuer die Kachel auf der Uebersicht. */
|
||||
export function abwesenheitstageGesamt(jahre: Abwesenheitsjahr[]): number {
|
||||
return jahre.reduce((summe, j) => summe + j.tage, 0)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import type { GesundheitsArt } from '@prisma/client'
|
||||
|
||||
/**
|
||||
* Zahlen und Staffeln aus dem Pflegerecht - an EINER Stelle, jede mit
|
||||
* Stand-Datum.
|
||||
*
|
||||
* Der Gesetzgeber aendert diese Werte. Zuletzt zum 01.01.2024, als die
|
||||
* Leistungszuschlaege nach § 43c SGB XI von 5/25/45/70 auf 15/30/50/75
|
||||
* Prozent angehoben wurden. Wer solche Zahlen im Code verstreut, findet
|
||||
* beim naechsten Mal nicht alle - deshalb stehen sie hier und nirgends
|
||||
* sonst.
|
||||
*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Leistungszuschlaege nach § 43c SGB XI
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stand der Staffel. Bei jeder Anzeige mit ausgegeben, damit sofort
|
||||
* auffaellt, wenn die Zahlen alt sind.
|
||||
*/
|
||||
export const LEISTUNGSZUSCHLAG_STAND = '01.01.2024'
|
||||
|
||||
/**
|
||||
* Quelle: § 43c Abs. 1 SGB XI in der Fassung des Pflegeunterstuetzungs-
|
||||
* und -entlastungsgesetzes, gueltig ab 01.01.2024.
|
||||
*
|
||||
* ACHTUNG bei der Auslegung der Grenzen: Das Gesetz spricht von Jahren
|
||||
* ("im ersten Jahr", "im zweiten Jahr" ...), gerechnet ab dem Beginn der
|
||||
* vollstationaeren Pflege. Das erste Jahr sind die Monate 0 bis 11; ab dem
|
||||
* zwoelften vollendeten Monat greift die naechste Stufe.
|
||||
*
|
||||
* Der Zuschlag bezieht sich AUSSCHLIESSLICH auf den pflegebedingten
|
||||
* Eigenanteil (den einrichtungseinheitlichen Eigenanteil, EEE) - nicht auf
|
||||
* Unterkunft und Verpflegung, nicht auf die Investitionskosten und nicht
|
||||
* auf die Ausbildungsumlage. Das wird regelmaessig falsch gerechnet.
|
||||
*/
|
||||
export type Zuschlagsstufe = {
|
||||
/** Ab wie vielen vollendeten Monaten in der Einrichtung? */
|
||||
abMonat: number
|
||||
/** Bis einschliesslich; null heisst: nach oben offen. */
|
||||
bisMonat: number | null
|
||||
prozent: number
|
||||
bezeichnung: string
|
||||
}
|
||||
|
||||
export const LEISTUNGSZUSCHLAG_STAFFEL: readonly Zuschlagsstufe[] = [
|
||||
{ abMonat: 0, bisMonat: 11, prozent: 15, bezeichnung: 'erstes Jahr' },
|
||||
{ abMonat: 12, bisMonat: 23, prozent: 30, bezeichnung: 'zweites Jahr' },
|
||||
{ abMonat: 24, bisMonat: 35, prozent: 50, bezeichnung: 'drittes Jahr' },
|
||||
{ abMonat: 36, bisMonat: null, prozent: 75, bezeichnung: 'ab dem vierten Jahr' },
|
||||
] as const
|
||||
|
||||
export const LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE = '§ 43c SGB XI'
|
||||
|
||||
/**
|
||||
* Ab welchem Pflegegrad gibt es den Zuschlag ueberhaupt? Ab Pflegegrad 2 -
|
||||
* bei Pflegegrad 1 zahlt die Pflegekasse in der vollstationaeren Pflege
|
||||
* nur den Zuschuss nach § 43 Abs. 3 SGB XI, und § 43c greift nicht.
|
||||
*/
|
||||
export const LEISTUNGSZUSCHLAG_AB_PFLEGEGRAD = 2
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Pflegegrad
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const PFLEGEGRAD_BESCHRIFTUNG: Record<number, string> = {
|
||||
1: 'Pflegegrad 1 — geringe Beeinträchtigung',
|
||||
2: 'Pflegegrad 2 — erhebliche Beeinträchtigung',
|
||||
3: 'Pflegegrad 3 — schwere Beeinträchtigung',
|
||||
4: 'Pflegegrad 4 — schwerste Beeinträchtigung',
|
||||
5: 'Pflegegrad 5 — schwerste Beeinträchtigung mit besonderen Anforderungen',
|
||||
}
|
||||
|
||||
export const PFLEGEGRAD_RECHTSGRUNDLAGE_BEGUTACHTUNG = '§ 18 SGB XI'
|
||||
|
||||
/**
|
||||
* Wie lange sollte zwischen zwei Hoeherstufungsantraegen mindestens
|
||||
* liegen? Keine gesetzliche Frist - ein Antrag ist jederzeit moeglich.
|
||||
* Sechs Monate sind der Erfahrungswert, ab dem eine Verschlechterung
|
||||
* dokumentiert genug ist, um nicht sofort wieder abgelehnt zu werden.
|
||||
*/
|
||||
export const HOEHERSTUFUNG_ABSTAND_MONATE = 6
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Abwesenheit
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wie viele Abwesenheitstage am Stueck werden ueblicherweise voll
|
||||
* berechnet, bevor der Heimplatz reduziert abgerechnet wird? Das steht im
|
||||
* HEIMVERTRAG, nicht im Gesetz - der Rahmenvertrag nach § 75 SGB XI
|
||||
* regelt es je Bundesland unterschiedlich. Die Zahl hier ist ein
|
||||
* Anhaltspunkt fuer den Hinweistext, keine Berechnungsgrundlage.
|
||||
*/
|
||||
export const ABWESENHEIT_HINWEIS_AB_TAGEN = 3
|
||||
|
||||
export const ABWESENHEIT_HINWEIS =
|
||||
'Abwesenheitstage — vor allem Krankenhausaufenthalte — können den Eigenanteil ' +
|
||||
'mindern: Der Heimplatz wird ab einer im Vertrag geregelten Zahl von Tagen nur ' +
|
||||
'noch teilweise berechnet. Maßgeblich ist der Heimvertrag, nicht das Gesetz. ' +
|
||||
'Sieh dort nach, ab wann gekürzt wird und wie hoch der Abschlag ist, und prüfe ' +
|
||||
'jede Heimrechnung daraufhin. Das wird regelmäßig übersehen.'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Gesundheitsereignisse
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const GESUNDHEITS_ART_BESCHRIFTUNG: Record<GesundheitsArt, string> = {
|
||||
ARZTBESUCH: 'Arztbesuch',
|
||||
KRANKENHAUS: 'Krankenhausaufenthalt',
|
||||
EXAZERBATION: 'Exazerbation (COPD-Verschlechterung)',
|
||||
STURZ: 'Sturz',
|
||||
MEDIKATIONSAENDERUNG: 'Medikationsänderung',
|
||||
SAUERSTOFFVERSORGUNG: 'Sauerstoffversorgung',
|
||||
INHALATION: 'Inhalation',
|
||||
HILFSMITTEL: 'Hilfsmittel',
|
||||
IMPFUNG: 'Impfung',
|
||||
REHA: 'Rehabilitation',
|
||||
PFLEGEBEGUTACHTUNG: 'Pflegebegutachtung',
|
||||
HEILMITTEL: 'Heilmittel (Physio-, Ergo-, Logopädie)',
|
||||
SONSTIGES: 'Sonstiges',
|
||||
}
|
||||
|
||||
/** Reihenfolge in der Schnellerfassung: das Häufigste zuerst. */
|
||||
export const SCHNELLERFASSUNG_REIHENFOLGE: GesundheitsArt[] = [
|
||||
'EXAZERBATION',
|
||||
'KRANKENHAUS',
|
||||
'SAUERSTOFFVERSORGUNG',
|
||||
'MEDIKATIONSAENDERUNG',
|
||||
'INHALATION',
|
||||
'STURZ',
|
||||
'HILFSMITTEL',
|
||||
'IMPFUNG',
|
||||
'ARZTBESUCH',
|
||||
'PFLEGEBEGUTACHTUNG',
|
||||
'REHA',
|
||||
'HEILMITTEL',
|
||||
'SONSTIGES',
|
||||
]
|
||||
|
||||
export const EXAZERBATION_SCHWEREGRADE = [
|
||||
{ wert: 'leicht', beschriftung: 'leicht — nur Bedarfsmedikation gesteigert' },
|
||||
{ wert: 'mittel', beschriftung: 'mittel — Antibiotikum und/oder Kortison' },
|
||||
{ wert: 'schwer', beschriftung: 'schwer — Krankenhausbehandlung nötig' },
|
||||
] as const
|
||||
|
||||
export const EXAZERBATION_AUSLOESER = [
|
||||
'Atemwegsinfekt',
|
||||
'Lungenentzündung',
|
||||
'unklar',
|
||||
'Kälte / Witterung',
|
||||
'Belastung',
|
||||
'Medikament abgesetzt',
|
||||
'Sonstiges',
|
||||
] as const
|
||||
|
||||
export const HILFSMITTEL_TYPISCH = [
|
||||
'Rollator',
|
||||
'Rollstuhl',
|
||||
'Sauerstoffkonzentrator',
|
||||
'mobile Sauerstoffflasche',
|
||||
'Pulsoxymeter',
|
||||
'Inhalationsgerät',
|
||||
'Pflegebett',
|
||||
'Toilettenstuhl',
|
||||
'Hausnotruf',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Warum die COPD hier ueberall auftaucht: Der Verlauf ist die Grundlage
|
||||
* fuer zwei Dinge, die Geld und Versorgung bedeuten - den Jahresbericht
|
||||
* ans Betreuungsgericht und den Antrag auf Hoeherstufung des Pflegegrades.
|
||||
* Wer Exazerbationen und Krankenhaustage nicht mitzaehlt, kann bei der
|
||||
* Begutachtung nicht belegen, dass sich der Zustand verschlechtert hat.
|
||||
*/
|
||||
export const COPD_HINWEIS =
|
||||
'Eine COPD verläuft in Schüben. Zwischen zwei Exazerbationen wirkt der Zustand ' +
|
||||
'stabil — die Verschlechterung zeigt sich erst in der Zahl der Schübe und der ' +
|
||||
'Krankenhaustage über das Jahr. Genau diese Zahlen brauchst du für den ' +
|
||||
'Jahresbericht und für einen Höherstufungsantrag.'
|
||||
@@ -0,0 +1,181 @@
|
||||
import { differenceInMonths } from 'date-fns'
|
||||
import { heuteAlsTag, plusMonate, tageZwischen, tagesdatum } from '@/lib/fristen/datum'
|
||||
import {
|
||||
LEISTUNGSZUSCHLAG_AB_PFLEGEGRAD,
|
||||
LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE,
|
||||
LEISTUNGSZUSCHLAG_STAFFEL,
|
||||
LEISTUNGSZUSCHLAG_STAND,
|
||||
type Zuschlagsstufe,
|
||||
} from '@/lib/pflege/konstanten'
|
||||
|
||||
/**
|
||||
* Leistungszuschlag nach § 43c SGB XI.
|
||||
*
|
||||
* Die Pflegekasse uebernimmt einen wachsenden Anteil des pflegebedingten
|
||||
* Eigenanteils, je laenger jemand in der Einrichtung lebt. Der Sprung von
|
||||
* 15 auf 30 Prozent nach zwoelf Monaten ist bares Geld - bei einem
|
||||
* Eigenanteil von 1.500 Euro sind das 225 Euro im Monat.
|
||||
*
|
||||
* Er wird oft uebersehen, weil er nicht beantragt werden muss: Die
|
||||
* Einrichtung rechnet ihn mit der Pflegekasse ab. Genau deshalb lohnt der
|
||||
* Blick auf die Heimrechnung - wenn die Stufe nicht umgestellt wurde,
|
||||
* merkt es sonst niemand.
|
||||
*
|
||||
* Hier wird nur gerechnet und angezeigt. Die monatliche Gegenueberstellung
|
||||
* von Heimrechnung und Deckungsquellen baut das Sozialhilfemodul unter
|
||||
* /sozialhilfe/heimkosten - diese Datei doppelt sie ausdruecklich nicht.
|
||||
*/
|
||||
|
||||
export { LEISTUNGSZUSCHLAG_STAFFEL, LEISTUNGSZUSCHLAG_STAND, LEISTUNGSZUSCHLAG_RECHTSGRUNDLAGE }
|
||||
|
||||
/**
|
||||
* Vollendete Monate in der Einrichtung. Der Einzugstag zaehlt mit: Wer am
|
||||
* 15.01. eingezogen ist, hat am 15.02. einen vollen Monat hinter sich.
|
||||
*/
|
||||
export function verweildauerMonate(einzugAm: Date, stichtag: Date): number {
|
||||
const von = tagesdatum(einzugAm)
|
||||
const bis = tagesdatum(stichtag)
|
||||
if (bis.getTime() < von.getTime()) return 0
|
||||
// date-fns rechnet auf Ortszeit - beide Daten auf die Mittagsstunde
|
||||
// heben, sonst verschiebt die Sommerzeit den Monatswechsel.
|
||||
const mittags = (d: Date) => new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 12)
|
||||
return Math.max(0, differenceInMonths(mittags(bis), mittags(von)))
|
||||
}
|
||||
|
||||
export function stufeFuerMonate(monate: number): Zuschlagsstufe {
|
||||
for (const stufe of LEISTUNGSZUSCHLAG_STAFFEL) {
|
||||
if (monate >= stufe.abMonat && (stufe.bisMonat === null || monate <= stufe.bisMonat)) {
|
||||
return stufe
|
||||
}
|
||||
}
|
||||
// Die Staffel ist nach oben offen; der Fall kann nicht eintreten.
|
||||
return LEISTUNGSZUSCHLAG_STAFFEL[LEISTUNGSZUSCHLAG_STAFFEL.length - 1]!
|
||||
}
|
||||
|
||||
export function naechsteStufeNach(stufe: Zuschlagsstufe): Zuschlagsstufe | null {
|
||||
const index = LEISTUNGSZUSCHLAG_STAFFEL.indexOf(stufe)
|
||||
if (index < 0 || index + 1 >= LEISTUNGSZUSCHLAG_STAFFEL.length) return null
|
||||
return LEISTUNGSZUSCHLAG_STAFFEL[index + 1] ?? null
|
||||
}
|
||||
|
||||
/** Prozentsatz auf einen Cent-Betrag, kaufmaennisch gerundet. */
|
||||
export function anteilCent(betragCent: bigint, prozent: number): bigint {
|
||||
const zaehler = betragCent * BigInt(prozent) * 2n
|
||||
const nenner = 200n
|
||||
const rest = zaehler % nenner
|
||||
const ganz = zaehler / nenner
|
||||
return rest * 2n >= nenner ? ganz + 1n : ganz
|
||||
}
|
||||
|
||||
export type Zuschlagsergebnis = {
|
||||
/** Laesst sich rechnen? Ohne Einzugsdatum nicht. */
|
||||
berechenbar: boolean
|
||||
verweildauerMonate: number
|
||||
stufe: Zuschlagsstufe | null
|
||||
prozent: number
|
||||
/** Was die Pflegekasse vom Eigenanteil uebernimmt. */
|
||||
zuschlagCent: bigint | null
|
||||
/** Was nach dem Zuschlag vom pflegebedingten Eigenanteil bleibt. */
|
||||
verbleibtCent: bigint | null
|
||||
naechsteStufe: {
|
||||
stufe: Zuschlagsstufe
|
||||
/** Ab diesem Tag greift die naechste Stufe. */
|
||||
abDatum: Date
|
||||
tageBis: number
|
||||
/** Wie viel mehr uebernimmt die Kasse dann im Monat? */
|
||||
mehrCent: bigint | null
|
||||
} | null
|
||||
hinweise: string[]
|
||||
}
|
||||
|
||||
export type ZuschlagEingabe = {
|
||||
einzugAm: Date | null
|
||||
/** Der pflegebedingte Eigenanteil (EEE) im Monat. */
|
||||
eigenanteilCent: bigint | null
|
||||
pflegegrad?: number | null
|
||||
stichtag?: Date
|
||||
}
|
||||
|
||||
export function leistungszuschlagBerechnen(eingabe: ZuschlagEingabe): Zuschlagsergebnis {
|
||||
const stichtag = tagesdatum(eingabe.stichtag ?? heuteAlsTag())
|
||||
const hinweise: string[] = []
|
||||
|
||||
if (!eingabe.einzugAm) {
|
||||
return {
|
||||
berechenbar: false,
|
||||
verweildauerMonate: 0,
|
||||
stufe: null,
|
||||
prozent: 0,
|
||||
zuschlagCent: null,
|
||||
verbleibtCent: null,
|
||||
naechsteStufe: null,
|
||||
hinweise: [
|
||||
'Ohne Einzugsdatum lässt sich der Leistungszuschlag nicht bestimmen. ' +
|
||||
'Trag es im Heimvertrag nach.',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
eingabe.pflegegrad !== null &&
|
||||
eingabe.pflegegrad !== undefined &&
|
||||
eingabe.pflegegrad < LEISTUNGSZUSCHLAG_AB_PFLEGEGRAD
|
||||
) {
|
||||
hinweise.push(
|
||||
`Der Leistungszuschlag setzt mindestens Pflegegrad ${LEISTUNGSZUSCHLAG_AB_PFLEGEGRAD} ` +
|
||||
`voraus. Bei Pflegegrad 1 zahlt die Pflegekasse in der vollstationären Pflege ` +
|
||||
`nur den Zuschuss nach § 43 Abs. 3 SGB XI.`,
|
||||
)
|
||||
}
|
||||
|
||||
const monate = verweildauerMonate(eingabe.einzugAm, stichtag)
|
||||
const stufe = stufeFuerMonate(monate)
|
||||
const eigenanteil = eingabe.eigenanteilCent
|
||||
const zuschlag = eigenanteil !== null ? anteilCent(eigenanteil, stufe.prozent) : null
|
||||
|
||||
const folge = naechsteStufeNach(stufe)
|
||||
let naechsteStufe: Zuschlagsergebnis['naechsteStufe'] = null
|
||||
if (folge) {
|
||||
const abDatum = plusMonate(tagesdatum(eingabe.einzugAm), folge.abMonat)
|
||||
const mehr =
|
||||
eigenanteil !== null
|
||||
? anteilCent(eigenanteil, folge.prozent) - anteilCent(eigenanteil, stufe.prozent)
|
||||
: null
|
||||
naechsteStufe = {
|
||||
stufe: folge,
|
||||
abDatum,
|
||||
tageBis: tageZwischen(stichtag, abDatum),
|
||||
mehrCent: mehr,
|
||||
}
|
||||
}
|
||||
|
||||
if (eigenanteil === null) {
|
||||
hinweise.push(
|
||||
'Ohne den pflegebedingten Eigenanteil aus dem Heimvertrag lässt sich nur der ' +
|
||||
'Prozentsatz anzeigen, nicht der Betrag.',
|
||||
)
|
||||
}
|
||||
|
||||
hinweise.push(
|
||||
'Der Zuschlag gilt nur für den pflegebedingten Eigenanteil (den ' +
|
||||
'einrichtungseinheitlichen Eigenanteil). Unterkunft, Verpflegung, ' +
|
||||
'Investitionskosten und Ausbildungsumlage bleiben davon unberührt — dort ' +
|
||||
'ändert sich mit der Stufe nichts.',
|
||||
)
|
||||
hinweise.push(
|
||||
'Der Zuschlag muss nicht beantragt werden; die Einrichtung rechnet ihn mit der ' +
|
||||
'Pflegekasse ab. Prüf trotzdem die Heimrechnung nach dem Stufenwechsel — ' +
|
||||
'nicht umgestellte Stufen fallen sonst niemandem auf.',
|
||||
)
|
||||
|
||||
return {
|
||||
berechenbar: true,
|
||||
verweildauerMonate: monate,
|
||||
stufe,
|
||||
prozent: stufe.prozent,
|
||||
zuschlagCent: zuschlag,
|
||||
verbleibtCent: eigenanteil !== null && zuschlag !== null ? eigenanteil - zuschlag : null,
|
||||
naechsteStufe,
|
||||
hinweise,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import type { GesundheitsArt } from '@prisma/client'
|
||||
import { tagesdatum } from '@/lib/fristen/datum'
|
||||
import {
|
||||
abwesenheitstageJeJahr,
|
||||
tageEines,
|
||||
type AbwesenheitsEreignis,
|
||||
} from '@/lib/pflege/abwesenheit'
|
||||
|
||||
/**
|
||||
* Auswertung des Gesundheitsverlaufs.
|
||||
*
|
||||
* Zwei Zahlen wandern in den Jahresbericht und in jeden
|
||||
* Hoeherstufungsantrag: Wie viele Exazerbationen gab es im Jahr, und wie
|
||||
* viele Tage lag die betreute Person im Krankenhaus. Beides ist bei einer
|
||||
* COPD der belastbare Beleg dafuer, dass sich der Zustand verschlechtert -
|
||||
* "es geht ihr schlechter" allein ueberzeugt keinen Gutachter.
|
||||
*
|
||||
* Reine Logik ohne Datenbank; die Beschreibungen der Ereignisse bleiben
|
||||
* verschluesselt und werden hier gar nicht angefasst.
|
||||
*/
|
||||
|
||||
export type VerlaufEreignis = {
|
||||
datum: Date
|
||||
bisDatum: Date | null
|
||||
art: GesundheitsArt
|
||||
abwesenheitstage: number | null
|
||||
/** Titel im Klartext - er enthaelt keine Diagnose, nur eine Bezeichnung. */
|
||||
titel?: string
|
||||
}
|
||||
|
||||
export type Verlaufsjahr = {
|
||||
jahr: number
|
||||
exazerbationen: number
|
||||
/** Exazerbationen, die zu einem Krankenhausaufenthalt gefuehrt haben. */
|
||||
krankenhausaufenthalte: number
|
||||
krankenhaustage: number
|
||||
abwesenheitstage: number
|
||||
stuerze: number
|
||||
ereignisseGesamt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Exazerbationen je Kalenderjahr.
|
||||
*
|
||||
* Gezaehlt wird das Jahr des Beginns. Ein Schub, der ueber Silvester
|
||||
* laeuft, ist ein Ereignis und gehoert in das Jahr, in dem er begann.
|
||||
*/
|
||||
export function exazerbationenJeJahr(
|
||||
ereignisse: VerlaufEreignis[],
|
||||
): { jahr: number; anzahl: number }[] {
|
||||
const jahre = new Map<number, number>()
|
||||
for (const ereignis of ereignisse) {
|
||||
if (ereignis.art !== 'EXAZERBATION') continue
|
||||
const jahr = tagesdatum(ereignis.datum).getUTCFullYear()
|
||||
jahre.set(jahr, (jahre.get(jahr) ?? 0) + 1)
|
||||
}
|
||||
return [...jahre.entries()]
|
||||
.map(([jahr, anzahl]) => ({ jahr, anzahl }))
|
||||
.sort((a, b) => b.jahr - a.jahr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Krankenhaustage je Kalenderjahr. Gerechnet wird wie bei den
|
||||
* Abwesenheitstagen: Tage zwischen Aufnahme und Entlassung, ueber den
|
||||
* Jahreswechsel aufgeteilt.
|
||||
*/
|
||||
export function krankenhaustageJeJahr(
|
||||
ereignisse: VerlaufEreignis[],
|
||||
): { jahr: number; tage: number }[] {
|
||||
const nurKrankenhaus: AbwesenheitsEreignis[] = ereignisse
|
||||
.filter((e) => e.art === 'KRANKENHAUS')
|
||||
.map((e) => ({
|
||||
datum: e.datum,
|
||||
bisDatum: e.bisDatum,
|
||||
abwesenheitstage: e.abwesenheitstage,
|
||||
art: e.art,
|
||||
}))
|
||||
return abwesenheitstageJeJahr(nurKrankenhaus).map((j) => ({ jahr: j.jahr, tage: j.tage }))
|
||||
}
|
||||
|
||||
/** Die vollstaendige Jahresauswertung, wie sie auf /pflege/verlauf steht. */
|
||||
export function verlaufAuswerten(ereignisse: VerlaufEreignis[]): Verlaufsjahr[] {
|
||||
const jahre = new Map<number, Verlaufsjahr>()
|
||||
|
||||
const holen = (jahr: number): Verlaufsjahr => {
|
||||
let eintrag = jahre.get(jahr)
|
||||
if (!eintrag) {
|
||||
eintrag = {
|
||||
jahr,
|
||||
exazerbationen: 0,
|
||||
krankenhausaufenthalte: 0,
|
||||
krankenhaustage: 0,
|
||||
abwesenheitstage: 0,
|
||||
stuerze: 0,
|
||||
ereignisseGesamt: 0,
|
||||
}
|
||||
jahre.set(jahr, eintrag)
|
||||
}
|
||||
return eintrag
|
||||
}
|
||||
|
||||
for (const ereignis of ereignisse) {
|
||||
const jahr = tagesdatum(ereignis.datum).getUTCFullYear()
|
||||
const eintrag = holen(jahr)
|
||||
eintrag.ereignisseGesamt += 1
|
||||
if (ereignis.art === 'EXAZERBATION') eintrag.exazerbationen += 1
|
||||
if (ereignis.art === 'STURZ') eintrag.stuerze += 1
|
||||
if (ereignis.art === 'KRANKENHAUS') eintrag.krankenhausaufenthalte += 1
|
||||
}
|
||||
|
||||
for (const zeile of krankenhaustageJeJahr(ereignisse)) {
|
||||
holen(zeile.jahr).krankenhaustage = zeile.tage
|
||||
}
|
||||
|
||||
const abwesend = abwesenheitstageJeJahr(
|
||||
ereignisse.map((e) => ({
|
||||
datum: e.datum,
|
||||
bisDatum: e.bisDatum,
|
||||
abwesenheitstage: e.abwesenheitstage,
|
||||
art: e.art,
|
||||
})),
|
||||
)
|
||||
for (const zeile of abwesend) {
|
||||
holen(zeile.jahr).abwesenheitstage = zeile.tage
|
||||
}
|
||||
|
||||
return [...jahre.values()].sort((a, b) => b.jahr - a.jahr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurzer Satz fuer den Jahresbericht - fertig zum Hineinkopieren.
|
||||
* Bewusst nuechtern und ohne Diagnosen: Der Bericht geht ans Gericht,
|
||||
* nicht an die Krankenkasse.
|
||||
*/
|
||||
export function verlaufZusammenfassung(jahr: Verlaufsjahr): string {
|
||||
const teile: string[] = []
|
||||
if (jahr.exazerbationen > 0) {
|
||||
teile.push(
|
||||
jahr.exazerbationen === 1
|
||||
? 'eine akute Verschlechterung der Atemwegserkrankung'
|
||||
: `${jahr.exazerbationen} akute Verschlechterungen der Atemwegserkrankung`,
|
||||
)
|
||||
}
|
||||
if (jahr.krankenhausaufenthalte > 0) {
|
||||
teile.push(
|
||||
`${jahr.krankenhausaufenthalte} Krankenhausaufenthalt${
|
||||
jahr.krankenhausaufenthalte === 1 ? '' : 'e'
|
||||
} mit insgesamt ${jahr.krankenhaustage} Tagen`,
|
||||
)
|
||||
}
|
||||
if (jahr.stuerze > 0) {
|
||||
teile.push(`${jahr.stuerze} dokumentierte${jahr.stuerze === 1 ? 'r' : ''} Sturz${jahr.stuerze === 1 ? '' : 'e'}`)
|
||||
}
|
||||
|
||||
if (teile.length === 0) {
|
||||
return `Im Jahr ${jahr.jahr} wurden keine besonderen gesundheitlichen Ereignisse erfasst.`
|
||||
}
|
||||
|
||||
return `Im Jahr ${jahr.jahr} sind dokumentiert: ${teile.join(', ')}.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Steigt die Zahl der Exazerbationen gegenueber dem Vorjahr? Genau das ist
|
||||
* das Argument fuer eine Neubegutachtung nach § 18 SGB XI.
|
||||
*/
|
||||
export function verschlechterungErkannt(
|
||||
jahre: Verlaufsjahr[],
|
||||
bezugsjahr: number,
|
||||
): { erkannt: boolean; begruendung: string | null } {
|
||||
const aktuell = jahre.find((j) => j.jahr === bezugsjahr)
|
||||
const vorjahr = jahre.find((j) => j.jahr === bezugsjahr - 1)
|
||||
if (!aktuell || !vorjahr) return { erkannt: false, begruendung: null }
|
||||
|
||||
const mehrSchuebe = aktuell.exazerbationen > vorjahr.exazerbationen
|
||||
const mehrTage = aktuell.krankenhaustage > vorjahr.krankenhaustage
|
||||
|
||||
if (!mehrSchuebe && !mehrTage) return { erkannt: false, begruendung: null }
|
||||
|
||||
const teile: string[] = []
|
||||
if (mehrSchuebe) {
|
||||
teile.push(
|
||||
`${vorjahr.exazerbationen} Exazerbationen im Jahr ${vorjahr.jahr} gegenüber ` +
|
||||
`${aktuell.exazerbationen} im Jahr ${aktuell.jahr}`,
|
||||
)
|
||||
}
|
||||
if (mehrTage) {
|
||||
teile.push(
|
||||
`${vorjahr.krankenhaustage} Krankenhaustage im Jahr ${vorjahr.jahr} gegenüber ` +
|
||||
`${aktuell.krankenhaustage} im Jahr ${aktuell.jahr}`,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
erkannt: true,
|
||||
begruendung:
|
||||
`Der erfasste Verlauf zeigt eine Zunahme: ${teile.join(' und ')}. ` +
|
||||
`Das kann eine erneute Begutachtung rechtfertigen.`,
|
||||
}
|
||||
}
|
||||
|
||||
/** Summe der Tage eines einzelnen Ereignisses - fuer die Zeitstrahl-Zeile. */
|
||||
export { tageEines }
|
||||
Reference in New Issue
Block a user