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
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
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)
|
|
}
|