Files
betreuungs-cockpit/src/lib/finanzen/betreuungsjahr.ts
T
Florian Wilde 22b3a9b9e9 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
2026-08-22 02:16:22 +02:00

131 lines
4.3 KiB
TypeScript
Raw 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.
import { alsTag, heute, jahrePlus, monatePlus, tagePlus, vorOderGleich } from '@/lib/finanzen/datum'
/**
* Das Betreuungsjahr.
*
* Es laeuft ab dem Tag der Bestellung, nicht ab dem 1. Januar. Wer am
* 15.03.2025 bestellt wurde, legt fuer den Zeitraum 15.03.2025 bis
* 14.03.2026 Rechnung (§ 1865 Abs. 1 BGB), und die Aufwandspauschale
* nach § 1878 BGB verfaellt sechs Monate nach dessen Ende.
*
* Das ist der haeufigste Rechenfehler in selbstgebauten Tabellen: man
* rechnet das Kalenderjahr ab und wundert sich, warum das Gericht
* nachfragt. Deshalb steht die Rechnung hier an genau einer Stelle.
*
* Schaltjahrfall: Ist die Bestellung am 29.02. erfolgt, gibt es in
* Nicht-Schaltjahren keinen 29.02. Das Jahr endet dann am 27.02. und das
* folgende beginnt am 28.02. - so entsteht weder eine Luecke noch eine
* Ueberschneidung, und jeder Tag gehoert zu genau einem Betreuungsjahr.
*/
export type Betreuungsjahr = {
/** 1 = das erste Jahr ab Bestellung. */
nummer: number
/** Erster Tag, einschliesslich. */
von: Date
/** Letzter Tag, einschliesslich. */
bis: Date
/** "1. Betreuungsjahr (15.03.2025 – 14.03.2026)" - fuer Auswahllisten. */
bezeichnung: string
}
function formatKurz(datum: Date): string {
const tag = String(datum.getUTCDate()).padStart(2, '0')
const monat = String(datum.getUTCMonth() + 1).padStart(2, '0')
return `${tag}.${monat}.${datum.getUTCFullYear()}`
}
function bauen(nummer: number, von: Date, bis: Date): Betreuungsjahr {
return {
nummer,
von,
bis,
bezeichnung: `${nummer}. Betreuungsjahr (${formatKurz(von)} – ${formatKurz(bis)})`,
}
}
/**
* Das n-te Betreuungsjahr. nummer ist 1-basiert; 1 ist das Jahr, das am
* Tag der Bestellung beginnt.
*/
export function betreuungsjahr(bestellungAm: Date, nummer: number): Betreuungsjahr {
if (!Number.isInteger(nummer) || nummer < 1) {
throw new Error('Das Betreuungsjahr wird ab 1 gezählt.')
}
const start = alsTag(bestellungAm)
const von = jahrePlus(start, nummer - 1)
const bis = tagePlus(jahrePlus(start, nummer), -1)
return bauen(nummer, von, bis)
}
/**
* In welches Betreuungsjahr faellt ein Datum? Null, wenn es vor der
* Bestellung liegt - dafuer gibt es keine Rechnungslegung.
*/
export function betreuungsjahrZuDatum(bestellungAm: Date, datum: Date): Betreuungsjahr | null {
const start = alsTag(bestellungAm)
const ziel = alsTag(datum)
if (ziel.getTime() < start.getTime()) return null
// Grobe Schaetzung ueber die Jahresdifferenz, danach hoechstens zwei
// Korrekturschritte - das haelt auch den Schaltjahrfall aus.
let nummer = ziel.getUTCFullYear() - start.getUTCFullYear() + 1
if (nummer < 1) nummer = 1
for (let versuch = 0; versuch < 4; versuch++) {
const jahr = betreuungsjahr(start, nummer)
if (ziel.getTime() < jahr.von.getTime()) {
nummer -= 1
if (nummer < 1) return null
continue
}
if (ziel.getTime() > jahr.bis.getTime()) {
nummer += 1
continue
}
return jahr
}
return null
}
/** Das Betreuungsjahr, in dem wir heute stehen. */
export function laufendesBetreuungsjahr(
bestellungAm: Date,
stichtag: Date = heute(),
): Betreuungsjahr | null {
return betreuungsjahrZuDatum(bestellungAm, stichtag)
}
/**
* Alle Betreuungsjahre von der Bestellung bis einschliesslich des Jahres,
* in dem "bis" liegt. Fuer die Auswahlliste im Berichtsformular - das
* laufende Jahr steht mit dabei, denn eine Zwischenaufstellung ist
* jederzeit erlaubt.
*/
export function betreuungsjahre(bestellungAm: Date, bis: Date = heute()): Betreuungsjahr[] {
const letztes = betreuungsjahrZuDatum(bestellungAm, bis)
if (!letztes) return []
const liste: Betreuungsjahr[] = []
for (let nummer = 1; nummer <= letztes.nummer; nummer++) {
liste.push(betreuungsjahr(bestellungAm, nummer))
}
return liste
}
/**
* Ist das Betreuungsjahr abgeschlossen? Nur dann ist die Rechnungslegung
* faellig; vorher ist der Bericht eine Zwischenaufstellung.
*/
export function jahrAbgeschlossen(jahr: Betreuungsjahr, stichtag: Date = heute()): boolean {
return vorOderGleich(jahr.bis, stichtag)
}
/**
* Ende der Ausschlussfrist fuer die Aufwandspauschale nach § 1878 BGB:
* sechs Monate nach dem Ende des Betreuungsjahres. Danach ist der
* Anspruch weg - kein Ermessen, keine Nachsicht.
*/
export function aufwandspauschaleBis(jahr: Betreuungsjahr): Date {
return monatePlus(jahr.bis, 6)
}