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,88 @@
|
||||
/**
|
||||
* Inventarliste der Wertgegenstaende.
|
||||
*
|
||||
* Das Schema kennt kein eigenes Inventarstueck (siehe
|
||||
* docs/schema-wuensche/substanz.md, Punkt 3). Gefuehrt wird jedes Stueck
|
||||
* deshalb als Vermoegensposten mit der Art WERTGEGENSTAND, verknuepft mit
|
||||
* der Immobilie; der geschaetzte Wert liegt als Wertstand mit der Quelle
|
||||
* "eigene Schaetzung".
|
||||
*
|
||||
* Raum und Verbleib passen in kein vorhandenes Feld. Sie stehen deshalb
|
||||
* als beschriftete Zeilen im ohnehin verschluesselten Notizfeld -
|
||||
* geschrieben und gelesen ausschliesslich hier, damit das Format an genau
|
||||
* einer Stelle steht und sich spaeter in eine echte Tabelle migrieren
|
||||
* laesst.
|
||||
*/
|
||||
|
||||
export const VERBLEIB_MOEGLICHKEITEN = [
|
||||
'im Haus',
|
||||
'im Heimzimmer',
|
||||
'bei Angehörigen',
|
||||
'eingelagert',
|
||||
'verkauft',
|
||||
'verschenkt',
|
||||
'entsorgt',
|
||||
'unbekannt',
|
||||
] as const
|
||||
export type Verbleib = (typeof VERBLEIB_MOEGLICHKEITEN)[number]
|
||||
|
||||
export type InventarNotiz = {
|
||||
raum: string | null
|
||||
verbleib: string | null
|
||||
bemerkung: string | null
|
||||
}
|
||||
|
||||
const MARKE_RAUM = 'Raum:'
|
||||
const MARKE_VERBLEIB = 'Verbleib:'
|
||||
const MARKE_BEMERKUNG = 'Bemerkung:'
|
||||
|
||||
/** Baut den Notiztext, der verschluesselt gespeichert wird. */
|
||||
export function inventarNotizSchreiben(notiz: InventarNotiz): string | null {
|
||||
const zeilen: string[] = []
|
||||
if (notiz.raum) zeilen.push(`${MARKE_RAUM} ${notiz.raum}`)
|
||||
if (notiz.verbleib) zeilen.push(`${MARKE_VERBLEIB} ${notiz.verbleib}`)
|
||||
if (notiz.bemerkung) zeilen.push(`${MARKE_BEMERKUNG} ${notiz.bemerkung}`)
|
||||
return zeilen.length > 0 ? zeilen.join('\n') : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den Notiztext zurueck. Was nicht dem Format entspricht - etwa eine
|
||||
* von Hand geschriebene Notiz aus einem anderen Modul - landet in
|
||||
* "bemerkung", damit nichts verloren geht.
|
||||
*/
|
||||
export function inventarNotizLesen(text: string | null | undefined): InventarNotiz {
|
||||
const notiz: InventarNotiz = { raum: null, verbleib: null, bemerkung: null }
|
||||
if (!text) return notiz
|
||||
|
||||
const rest: string[] = []
|
||||
for (const zeile of text.split('\n')) {
|
||||
const getrimmt = zeile.trim()
|
||||
if (getrimmt.startsWith(MARKE_RAUM)) {
|
||||
notiz.raum = getrimmt.slice(MARKE_RAUM.length).trim() || null
|
||||
} else if (getrimmt.startsWith(MARKE_VERBLEIB)) {
|
||||
notiz.verbleib = getrimmt.slice(MARKE_VERBLEIB.length).trim() || null
|
||||
} else if (getrimmt.startsWith(MARKE_BEMERKUNG)) {
|
||||
notiz.bemerkung = getrimmt.slice(MARKE_BEMERKUNG.length).trim() || null
|
||||
} else if (getrimmt !== '') {
|
||||
rest.push(getrimmt)
|
||||
}
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
notiz.bemerkung = [notiz.bemerkung, ...rest].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
return notiz
|
||||
}
|
||||
|
||||
/** Die Quelle, mit der ein geschaetzter Inventarwert abgelegt wird. */
|
||||
export const INVENTAR_WERTQUELLE = 'eigene Schätzung'
|
||||
|
||||
/**
|
||||
* Summe der geschaetzten Werte. Wichtig fuer die Frage, ob sich eine
|
||||
* Haushaltsaufloesung ueber einen Verwerter ueberhaupt lohnt - meist tut
|
||||
* sie es nicht, und dann gehoert das begruendet in die Akte.
|
||||
*/
|
||||
export function inventarSumme(stuecke: { wertCent: bigint | null }[]): bigint {
|
||||
return stuecke.reduce((summe, stueck) => summe + (stueck.wertCent ?? 0n), 0n)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { BuchungKategorie, ImmobilienStatusSozialhilfe } from '@prisma/client'
|
||||
|
||||
/**
|
||||
* Feste Begriffe rund um das Hausgrundstueck.
|
||||
*
|
||||
* Alles, was in der Oberflaeche als Auswahl erscheint, steht hier - und
|
||||
* daneben die Erklaerung, was der Begriff bedeutet. Grundbuchrecht ist
|
||||
* nichts, was man als ehrenamtlicher Betreuer im Kopf hat.
|
||||
*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Grundbuch
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const GRUNDBUCH_ABTEILUNGEN = ['II', 'III'] as const
|
||||
export type Grundbuchabteilung = (typeof GRUNDBUCH_ABTEILUNGEN)[number]
|
||||
|
||||
/**
|
||||
* Warum die Unterscheidung wichtig ist: Eine Grundschuld in Abteilung III
|
||||
* mindert den Wert des Grundstuecks um den noch valutierenden Betrag. Ein
|
||||
* Wohnrecht in Abteilung II mindert ihn ebenfalls, laesst sich aber nicht
|
||||
* einfach abloesen - es haengt am Leben des Berechtigten. Fuer die Frage,
|
||||
* was das Sozialamt als verwertbares Vermoegen ansieht, ist das der
|
||||
* Unterschied zwischen "verkaufen und Erloes einsetzen" und "praktisch
|
||||
* unverkaeuflich".
|
||||
*/
|
||||
export const GRUNDBUCH_ABTEILUNG_ERKLAERUNG: Record<Grundbuchabteilung, string> = {
|
||||
II:
|
||||
'Abteilung II: Lasten und Beschränkungen — alles, was jemand anderes am ' +
|
||||
'Grundstück darf oder was den Eigentümer einschränkt. Zum Beispiel ' +
|
||||
'Wohnrecht, Nießbrauch, Wegerecht, Reallast, Vorkaufsrecht, aber auch ' +
|
||||
'ein Betreuungs- oder Nacherbenvermerk.',
|
||||
III:
|
||||
'Abteilung III: Grundpfandrechte — Grundschulden und Hypotheken, also ' +
|
||||
'Sicherheiten für Geld. Der eingetragene Betrag ist die Höchstsumme, ' +
|
||||
'nicht die tatsächliche Restschuld: Ein längst getilgtes Darlehen kann ' +
|
||||
'mit voller Grundschuld im Grundbuch stehen. Für den Wert zählt, was die ' +
|
||||
'Bank noch zu fordern hat.',
|
||||
}
|
||||
|
||||
export const BELASTUNG_ARTEN = [
|
||||
'Grundschuld',
|
||||
'Hypothek',
|
||||
'Wohnrecht',
|
||||
'Nießbrauch',
|
||||
'Reallast',
|
||||
'Wegerecht',
|
||||
'Vorkaufsrecht',
|
||||
'Nacherbenvermerk',
|
||||
'Sonstiges',
|
||||
] as const
|
||||
export type Belastungsart = (typeof BELASTUNG_ARTEN)[number]
|
||||
|
||||
/** Welche Art gehoert ueblicherweise in welche Abteilung? Nur ein Vorschlag. */
|
||||
export const BELASTUNG_ART_ABTEILUNG: Record<Belastungsart, Grundbuchabteilung> = {
|
||||
Grundschuld: 'III',
|
||||
Hypothek: 'III',
|
||||
Wohnrecht: 'II',
|
||||
Nießbrauch: 'II',
|
||||
Reallast: 'II',
|
||||
Wegerecht: 'II',
|
||||
Vorkaufsrecht: 'II',
|
||||
Nacherbenvermerk: 'II',
|
||||
Sonstiges: 'II',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Bewohnt-Status
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const BEWOHNT_STATUS = ['leerstehend', 'vermietet', 'selbst bewohnt'] as const
|
||||
export type BewohntStatus = (typeof BEWOHNT_STATUS)[number]
|
||||
|
||||
export const BEWOHNT_STATUS_ERKLAERUNG: Record<BewohntStatus, string> = {
|
||||
leerstehend:
|
||||
'Niemand wohnt darin. Damit greifen die Pflichten bei Leerstand — ' +
|
||||
'Versicherer informieren, Frostschutz, Verkehrssicherung.',
|
||||
vermietet: 'Ein Mietverhältnis besteht. Mieteinnahmen gehören ins Kassenbuch.',
|
||||
'selbst bewohnt':
|
||||
'Die betreute Person bewohnt das Objekt selbst. Das ist die Voraussetzung ' +
|
||||
'für den Schutz als angemessenes Hausgrundstück nach § 90 Abs. 2 Nr. 8 SGB XII.',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Wertquellen
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pflichtangabe bei jedem Wertstand. Ein Wert ohne Quelle ist gegenueber
|
||||
* Gericht und Bezirk wertlos - die erste Rueckfrage ist immer "woher haben
|
||||
* Sie die Zahl?".
|
||||
*/
|
||||
export const WERTQUELLEN_IMMOBILIE = [
|
||||
'Bodenrichtwert',
|
||||
'Maklergutachten',
|
||||
'Verkehrswertgutachten',
|
||||
'eigene Schätzung',
|
||||
] as const
|
||||
export type WertquelleImmobilie = (typeof WERTQUELLEN_IMMOBILIE)[number]
|
||||
|
||||
export const WERTQUELLE_ERKLAERUNG: Record<WertquelleImmobilie, string> = {
|
||||
Bodenrichtwert:
|
||||
'Amtlicher Durchschnittswert je Quadratmeter Boden, kostenlos beim ' +
|
||||
'Gutachterausschuss bzw. im BORIS-Portal Bayern. Er bewertet nur den ' +
|
||||
'Grund und Boden, nicht das Gebäude — als Untergrenze brauchbar, als ' +
|
||||
'Verkehrswert nicht.',
|
||||
Maklergutachten:
|
||||
'Unverbindliche Einschätzung eines Maklers, meist kostenlos. Gut für ' +
|
||||
'einen ersten Anhalt und für das Gespräch mit dem Bezirk, aber kein ' +
|
||||
'Nachweis gegenüber dem Gericht.',
|
||||
Verkehrswertgutachten:
|
||||
'Gutachten eines öffentlich bestellten Sachverständigen nach § 194 BauGB. ' +
|
||||
'Kostet Geld, ist aber die belastbare Zahl — bei Verkauf und beim ' +
|
||||
'Genehmigungsantrag ans Betreuungsgericht in der Regel unverzichtbar.',
|
||||
'eigene Schätzung':
|
||||
'Meine eigene Einschätzung. Immer als solche kennzeichnen und ' +
|
||||
'begründen, damit später niemand sie für eine Bewertung hält.',
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Status im Sozialhilfeverfahren
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const IMMOBILIEN_STATUS_BESCHRIFTUNG: Record<ImmobilienStatusSozialhilfe, string> = {
|
||||
UNGEKLAERT: 'ungeklärt',
|
||||
SELBST_BEWOHNT_GESCHUETZT: 'selbst bewohnt und geschützt',
|
||||
HAERTEFALL_GELTEND_GEMACHT: 'Härtefall geltend gemacht',
|
||||
DARLEHEN_91_SGB_XII: 'Darlehen nach § 91 SGB XII',
|
||||
VERKAUF_LAEUFT: 'Verkauf läuft',
|
||||
VERWERTET: 'verwertet',
|
||||
}
|
||||
|
||||
export const IMMOBILIEN_STATUS_ERKLAERUNG: Record<ImmobilienStatusSozialhilfe, string> = {
|
||||
UNGEKLAERT:
|
||||
'Mit dem Sozialhilfeträger ist noch nicht besprochen, wie das Grundstück ' +
|
||||
'behandelt wird. Das ist der Ausgangszustand — er sollte nicht lange so bleiben.',
|
||||
SELBST_BEWOHNT_GESCHUETZT:
|
||||
'Ein angemessenes Hausgrundstück, das die betreute Person selbst bewohnt, ' +
|
||||
'bleibt geschützt (§ 90 Abs. 2 Nr. 8 SGB XII). Nach dem dauerhaften Einzug ' +
|
||||
'ins Heim entfällt dieser Schutz nach Auffassung der meisten Träger — ' +
|
||||
'genau hier wird gestritten.',
|
||||
HAERTEFALL_GELTEND_GEMACHT:
|
||||
'Die Verwertung wäre eine Härte (§ 90 Abs. 3 SGB XII), zum Beispiel weil ' +
|
||||
'ein Angehöriger dort lebt oder eine Rückkehr nicht ausgeschlossen ist. ' +
|
||||
'Die Begründung gehört schriftlich zur Akte.',
|
||||
DARLEHEN_91_SGB_XII:
|
||||
'Der Träger leistet vorerst als Darlehen und sichert es meist durch eine ' +
|
||||
'Grundschuld (§ 91 SGB XII). Das Haus bleibt zunächst, die Schuld wächst — ' +
|
||||
'die Höhe im Blick behalten.',
|
||||
VERKAUF_LAEUFT:
|
||||
'Der Verkauf ist eingeleitet. Achtung: Der Verkauf eines Grundstücks ist ' +
|
||||
'genehmigungspflichtig — die Genehmigung des Betreuungsgerichts muss vor ' +
|
||||
'dem Notartermin vorliegen.',
|
||||
VERWERTET: 'Das Objekt ist verkauft oder anderweitig verwertet, der Erlös vereinnahmt.',
|
||||
}
|
||||
|
||||
export const IMMOBILIEN_STATUS_RECHTSGRUNDLAGE = '§ 90 Abs. 2 Nr. 8, § 91 SGB XII'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Kostenkonto
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Buchungskategorien, die zum Objekt gehoeren. Buchungen legt das
|
||||
* Finanzmodul an; hier wird nur ausgewertet.
|
||||
*
|
||||
* Eine Zuordnung "Buchung gehoert zu diesem Objekt" gibt es im Schema
|
||||
* nicht (siehe docs/schema-wuensche/substanz.md). Solange nur ein
|
||||
* Grundstueck gefuehrt wird, ist die Zuordnung ueber die Kategorie
|
||||
* eindeutig - die Auswertung sagt das ausdruecklich dazu.
|
||||
*/
|
||||
export const IMMOBILIEN_AUSGABE_KATEGORIEN = [
|
||||
'GRUNDSTEUER',
|
||||
'IMMOBILIE_BETRIEBSKOSTEN',
|
||||
'IMMOBILIE_INSTANDHALTUNG',
|
||||
] as const satisfies readonly BuchungKategorie[]
|
||||
|
||||
export const IMMOBILIEN_EINNAHME_KATEGORIEN = [
|
||||
'MIETEINNAHME',
|
||||
] as const satisfies readonly BuchungKategorie[]
|
||||
|
||||
export const IMMOBILIEN_KATEGORIEN: BuchungKategorie[] = [
|
||||
...IMMOBILIEN_AUSGABE_KATEGORIEN,
|
||||
...IMMOBILIEN_EINNAHME_KATEGORIEN,
|
||||
]
|
||||
|
||||
export const IMMOBILIEN_KATEGORIE_BESCHRIFTUNG: Partial<Record<BuchungKategorie, string>> = {
|
||||
GRUNDSTEUER: 'Grundsteuer',
|
||||
IMMOBILIE_BETRIEBSKOSTEN: 'Betriebskosten (Gebäudeversicherung, Energie, Wasser, Schornsteinfeger)',
|
||||
IMMOBILIE_INSTANDHALTUNG: 'Instandhaltung und Reparatur',
|
||||
MIETEINNAHME: 'Mieteinnahme',
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { BuchungKategorie, BuchungRichtung } from '@prisma/client'
|
||||
import {
|
||||
IMMOBILIEN_AUSGABE_KATEGORIEN,
|
||||
IMMOBILIEN_EINNAHME_KATEGORIEN,
|
||||
} from '@/lib/immobilie/konstanten'
|
||||
|
||||
/**
|
||||
* Kostenkonto des Objekts.
|
||||
*
|
||||
* Nur lesend: Die Buchungen legt das Finanzmodul an. Hier werden sie je
|
||||
* Jahr und Kategorie zusammengezaehlt, damit im Jahresbericht und im
|
||||
* Gespraech mit dem Bezirk eine Zahl steht: "Das Haus kostet im Jahr X
|
||||
* Euro, ohne dass jemand darin wohnt."
|
||||
*
|
||||
* Genau diese Zahl ist das Argument in der Haertefall- und
|
||||
* Verwertungsdiskussion - und sie ist auch das Argument dafuer, ein
|
||||
* leerstehendes Haus nicht jahrelang stehen zu lassen.
|
||||
*/
|
||||
|
||||
export type KostenBuchung = {
|
||||
datum: Date
|
||||
betragCent: bigint
|
||||
richtung: BuchungRichtung
|
||||
kategorie: BuchungKategorie
|
||||
}
|
||||
|
||||
export type Kostenzeile = {
|
||||
kategorie: BuchungKategorie
|
||||
summeCent: bigint
|
||||
anzahl: number
|
||||
}
|
||||
|
||||
export type Kostenjahr = {
|
||||
jahr: number
|
||||
ausgabenCent: bigint
|
||||
einnahmenCent: bigint
|
||||
/** Einnahmen minus Ausgaben. Negativ heisst: Das Objekt kostet Geld. */
|
||||
saldoCent: bigint
|
||||
ausgaben: Kostenzeile[]
|
||||
einnahmen: Kostenzeile[]
|
||||
anzahlBuchungen: number
|
||||
}
|
||||
|
||||
const AUSGABEN = new Set<string>(IMMOBILIEN_AUSGABE_KATEGORIEN)
|
||||
const EINNAHMEN = new Set<string>(IMMOBILIEN_EINNAHME_KATEGORIEN)
|
||||
|
||||
export function istImmobilienbuchung(kategorie: BuchungKategorie): boolean {
|
||||
return AUSGABEN.has(kategorie) || EINNAHMEN.has(kategorie)
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Jahresuebersicht. Neueste Jahre zuerst - danach schaut man
|
||||
* zuerst.
|
||||
*
|
||||
* Umbuchungen bleiben aussen vor: Sie verschieben Geld zwischen eigenen
|
||||
* Konten und waeren in einer Kostenaufstellung eine Doppelzaehlung.
|
||||
*/
|
||||
export function kostenkontoAufbauen(buchungen: KostenBuchung[]): Kostenjahr[] {
|
||||
const jahre = new Map<number, Map<BuchungKategorie, Kostenzeile>>()
|
||||
|
||||
for (const buchung of buchungen) {
|
||||
if (buchung.richtung === 'UMBUCHUNG') continue
|
||||
if (!istImmobilienbuchung(buchung.kategorie)) continue
|
||||
|
||||
const jahr = buchung.datum.getUTCFullYear()
|
||||
let zeilen = jahre.get(jahr)
|
||||
if (!zeilen) {
|
||||
zeilen = new Map()
|
||||
jahre.set(jahr, zeilen)
|
||||
}
|
||||
|
||||
const vorhanden = zeilen.get(buchung.kategorie)
|
||||
if (vorhanden) {
|
||||
vorhanden.summeCent += buchung.betragCent
|
||||
vorhanden.anzahl += 1
|
||||
} else {
|
||||
zeilen.set(buchung.kategorie, {
|
||||
kategorie: buchung.kategorie,
|
||||
summeCent: buchung.betragCent,
|
||||
anzahl: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ergebnis: Kostenjahr[] = []
|
||||
|
||||
for (const [jahr, zeilen] of jahre) {
|
||||
const alle = [...zeilen.values()]
|
||||
const ausgaben = alle
|
||||
.filter((z) => AUSGABEN.has(z.kategorie))
|
||||
.sort((a, b) => Number(b.summeCent - a.summeCent))
|
||||
const einnahmen = alle
|
||||
.filter((z) => EINNAHMEN.has(z.kategorie))
|
||||
.sort((a, b) => Number(b.summeCent - a.summeCent))
|
||||
|
||||
const ausgabenCent = ausgaben.reduce((summe, z) => summe + z.summeCent, 0n)
|
||||
const einnahmenCent = einnahmen.reduce((summe, z) => summe + z.summeCent, 0n)
|
||||
|
||||
ergebnis.push({
|
||||
jahr,
|
||||
ausgaben,
|
||||
einnahmen,
|
||||
ausgabenCent,
|
||||
einnahmenCent,
|
||||
saldoCent: einnahmenCent - ausgabenCent,
|
||||
anzahlBuchungen: alle.reduce((summe, z) => summe + z.anzahl, 0),
|
||||
})
|
||||
}
|
||||
|
||||
return ergebnis.sort((a, b) => b.jahr - a.jahr)
|
||||
}
|
||||
|
||||
/** Durchschnittliche Jahreskosten ueber alle vollstaendig erfassten Jahre. */
|
||||
export function durchschnittlicheJahreskosten(jahre: Kostenjahr[], laufendesJahr: number): bigint {
|
||||
const abgeschlossen = jahre.filter((j) => j.jahr < laufendesJahr)
|
||||
if (abgeschlossen.length === 0) return 0n
|
||||
const summe = abgeschlossen.reduce((wert, j) => wert + j.ausgabenCent, 0n)
|
||||
return summe / BigInt(abgeschlossen.length)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { heuteAlsTag, plusJahre, plusTage, tag, tagesdatum } from '@/lib/fristen/datum'
|
||||
|
||||
/**
|
||||
* Pflichten bei Leerstand.
|
||||
*
|
||||
* Das ist der Teil, der einem als Betreuer wirklich um die Ohren fliegen
|
||||
* kann. Ein leerstehendes Haus ist kein ruhender Vermoegensposten, sondern
|
||||
* eine laufende Verpflichtung: Der Gebaeudeversicherer wird leistungsfrei,
|
||||
* wenn ihm der Leerstand nicht angezeigt wurde. Ein geplatztes Rohr im
|
||||
* Januar kostet dann den Schaden aus dem eigenen Vermoegen der betreuten
|
||||
* Person - und die Frage, warum niemand Bescheid gesagt hat, landet im
|
||||
* Jahresbericht.
|
||||
*
|
||||
* Deshalb traegt jeder Punkt hier nicht nur einen Titel, sondern auch die
|
||||
* Begruendung, warum er wichtig ist. Wer den Punkt abhakt, soll wissen,
|
||||
* wovor er sich gerade geschuetzt hat.
|
||||
*
|
||||
* Reine Logik, keine Datenbank: Die Liste wird beim Anlegen einer
|
||||
* leerstehenden Immobilie in ImmobilienCheckpunkt uebernommen.
|
||||
*/
|
||||
|
||||
export type LeerstandPunkt = {
|
||||
/** Stabiler Schluessel, damit die Uebernahme keine Dubletten erzeugt. */
|
||||
schluessel: string
|
||||
titel: string
|
||||
/** Warum ist der Punkt wichtig? Steht so in der Oberflaeche. */
|
||||
beschreibung: string
|
||||
faelligAm: Date
|
||||
/** iCalendar-RRULE fuer wiederkehrende Punkte, sonst undefined. */
|
||||
wiederholungRrule?: string
|
||||
reihenfolge: number
|
||||
/**
|
||||
* Punkte, bei denen ein Versaeumnis unmittelbar Geld kostet oder eine
|
||||
* Haftung ausloest. In der Oberflaeche rot.
|
||||
*/
|
||||
dringend: boolean
|
||||
}
|
||||
|
||||
/** Der Monat, in dem der Frostschutz stehen muss: Oktober. */
|
||||
export const FROSTSCHUTZ_MONAT = 10
|
||||
export const FROSTSCHUTZ_TAG = 1
|
||||
|
||||
/** Der Winterdienst muss organisiert sein, bevor der Frostschutz greift. */
|
||||
export const WINTERDIENST_MONAT = 10
|
||||
export const WINTERDIENST_TAG = 15
|
||||
|
||||
/**
|
||||
* Der naechste 1. Oktober ab einem Stichtag. Faellt der Stichtag selbst
|
||||
* auf den 1. Oktober, ist es dieser Tag - nicht erst der des Folgejahres.
|
||||
*/
|
||||
export function naechsterStichtagImJahr(
|
||||
ab: Date,
|
||||
monat: number,
|
||||
tagImMonat: number,
|
||||
): Date {
|
||||
const basis = tagesdatum(ab)
|
||||
const inDiesemJahr = tag(basis.getUTCFullYear(), monat, tagImMonat)
|
||||
if (inDiesemJahr.getTime() >= basis.getTime()) return inDiesemJahr
|
||||
return tag(basis.getUTCFullYear() + 1, monat, tagImMonat)
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt die Checkliste fuer ein leerstehendes Objekt.
|
||||
*
|
||||
* @param leerstandSeit Beginn des Leerstands. Fehlt er, wird ab heute gerechnet.
|
||||
* @param heute Bezugstag (Test).
|
||||
*/
|
||||
export function leerstandCheckliste(
|
||||
leerstandSeit?: Date | null,
|
||||
heute: Date = heuteAlsTag(),
|
||||
): LeerstandPunkt[] {
|
||||
const start = leerstandSeit ? tagesdatum(leerstandSeit) : tagesdatum(heute)
|
||||
// Faelligkeiten liegen nie in der Vergangenheit: Ein Punkt, der beim
|
||||
// Anlegen schon ueberfaellig waere, hilft niemandem weiter.
|
||||
const abHeute = (datum: Date) => (datum.getTime() < heute.getTime() ? tagesdatum(heute) : datum)
|
||||
|
||||
return [
|
||||
{
|
||||
schluessel: 'gebaeudeversicherung-leerstand-melden',
|
||||
titel: 'Gebäudeversicherer über den Leerstand informieren',
|
||||
beschreibung:
|
||||
'Schriftlich, am besten per Einschreiben, und die Antwort zur Akte nehmen. ' +
|
||||
'Leerstand ist eine Gefahrerhöhung: Wer sie nicht anzeigt, riskiert, dass ' +
|
||||
'der Versicherer im Schadensfall leistungsfrei wird — dann zahlt niemand ' +
|
||||
'den Wasserschaden. Viele Verträge verlangen bei Leerstand zusätzlich ' +
|
||||
'wöchentliche Kontrollgänge und abgestellte Wasserzufuhr. Frag ausdrücklich ' +
|
||||
'nach, welche Auflagen gelten, und halte sie schriftlich fest.',
|
||||
faelligAm: abHeute(plusTage(start, 7)),
|
||||
reihenfolge: 10,
|
||||
dringend: true,
|
||||
},
|
||||
{
|
||||
schluessel: 'kontrollgang',
|
||||
titel: 'Kontrollgang durch das Haus',
|
||||
beschreibung:
|
||||
'Einmal pro Woche durch alle Räume, Keller und Dachboden: Wasser, Schimmel, ' +
|
||||
'eingeschlagene Scheiben, Einbruchsspuren. Datum und Ergebnis notieren — ' +
|
||||
'die meisten Gebäudeversicherer verlangen bei Leerstand regelmäßige ' +
|
||||
'Kontrollen und wollen im Schadensfall den Nachweis sehen.',
|
||||
faelligAm: abHeute(plusTage(start, 7)),
|
||||
wiederholungRrule: 'FREQ=WEEKLY;INTERVAL=1',
|
||||
reihenfolge: 20,
|
||||
dringend: true,
|
||||
},
|
||||
{
|
||||
schluessel: 'frostschutz',
|
||||
titel: 'Frostschutz: Wasser abstellen, Leitungen entleeren, Heizung auf Frostschutz',
|
||||
beschreibung:
|
||||
'Haupthahn zu, Leitungen und Heizkörper entleeren oder die Heizung dauerhaft ' +
|
||||
'auf Frostschutzstellung — beides zusammen geht nicht. Ein einziges geplatztes ' +
|
||||
'Rohr richtet in einem leeren Haus über Wochen unbemerkt Schaden an. Jedes Jahr ' +
|
||||
'vor der ersten Frostnacht erledigen, deshalb im Oktober.',
|
||||
faelligAm: naechsterStichtagImJahr(heute, FROSTSCHUTZ_MONAT, FROSTSCHUTZ_TAG),
|
||||
wiederholungRrule: `FREQ=YEARLY;BYMONTH=${FROSTSCHUTZ_MONAT};BYMONTHDAY=${FROSTSCHUTZ_TAG}`,
|
||||
reihenfolge: 30,
|
||||
dringend: true,
|
||||
},
|
||||
{
|
||||
schluessel: 'winterdienst-verkehrssicherung',
|
||||
titel: 'Winterdienst und Verkehrssicherungspflicht organisieren',
|
||||
beschreibung:
|
||||
'Räum- und Streupflicht auf Gehweg und Grundstückszufahrt trifft den ' +
|
||||
'Eigentümer, auch wenn niemand im Haus wohnt. Dazu kommen lose Dachziegel, ' +
|
||||
'morsche Äste und der Zustand der Treppe. Wer hier nichts veranlasst, haftet ' +
|
||||
'persönlich für den Sturz eines Postboten. Schriftliche Beauftragung eines ' +
|
||||
'Dienstleisters oder Nachbarn, Nachweis zur Akte.',
|
||||
faelligAm: naechsterStichtagImJahr(heute, WINTERDIENST_MONAT, WINTERDIENST_TAG),
|
||||
wiederholungRrule: `FREQ=YEARLY;BYMONTH=${WINTERDIENST_MONAT};BYMONTHDAY=${WINTERDIENST_TAG}`,
|
||||
reihenfolge: 40,
|
||||
dringend: true,
|
||||
},
|
||||
{
|
||||
schluessel: 'zaehlerstaende',
|
||||
titel: 'Zählerstände ablesen und dokumentieren',
|
||||
beschreibung:
|
||||
'Strom, Gas, Wasser mit Datum und Foto festhalten. Nur so lassen sich ' +
|
||||
'Abschläge senken, Nachzahlungen prüfen und ein schleichender Wasserverlust ' +
|
||||
'überhaupt bemerken. Bei einem leeren Haus ist jeder Verbrauch erklärungs' +
|
||||
'bedürftig — auch gegenüber dem Bezirk.',
|
||||
faelligAm: abHeute(plusTage(start, 7)),
|
||||
wiederholungRrule: 'FREQ=MONTHLY;INTERVAL=3',
|
||||
reihenfolge: 50,
|
||||
dringend: false,
|
||||
},
|
||||
{
|
||||
schluessel: 'schluesselverzeichnis',
|
||||
titel: 'Schlüsselverzeichnis anlegen: wer hat welchen Schlüssel',
|
||||
beschreibung:
|
||||
'Jeder Schlüssel mit Nummer, Art (Haustür, Keller, Garage) und Person, ' +
|
||||
'Übergabedatum und Unterschrift. Bei einem Einbruch ohne Aufbruchspuren ' +
|
||||
'fragt der Versicherer als Erstes danach. Und beim späteren Verkauf muss ' +
|
||||
'vollständig übergeben werden.',
|
||||
faelligAm: abHeute(plusTage(start, 14)),
|
||||
reihenfolge: 60,
|
||||
dringend: false,
|
||||
},
|
||||
{
|
||||
schluessel: 'fotodokumentation',
|
||||
titel: 'Fotodokumentation vor der Räumung',
|
||||
beschreibung:
|
||||
'Jeden Raum vollständig fotografieren, bevor etwas bewegt wird, dazu ' +
|
||||
'Wertgegenstände einzeln. Das schützt vor dem Vorwurf, es sei etwas ' +
|
||||
'verschwunden, belegt den Zustand des Hauses gegenüber Käufern und ' +
|
||||
'Versicherern und ist die Grundlage der Inventarliste. Fotos als Dokument ' +
|
||||
'mit der Art „Foto“ hochladen und mit dem Objekt verknüpfen.',
|
||||
faelligAm: abHeute(plusTage(start, 14)),
|
||||
reihenfolge: 70,
|
||||
dringend: false,
|
||||
},
|
||||
{
|
||||
schluessel: 'briefkasten-nachsendeauftrag',
|
||||
titel: 'Briefkasten leeren und Nachsendeauftrag einrichten',
|
||||
beschreibung:
|
||||
'Ein überquellender Briefkasten zeigt jedem, dass niemand da ist — und ' +
|
||||
'wichtiger: Bescheide mit Rechtsbehelfsfristen gelten als zugegangen, auch ' +
|
||||
'wenn sie ungelesen im Kasten liegen. Nachsendeauftrag auf die eigene ' +
|
||||
'Anschrift, zusätzlich die Absender direkt umschreiben lassen.',
|
||||
faelligAm: abHeute(plusTage(start, 7)),
|
||||
reihenfolge: 80,
|
||||
dringend: true,
|
||||
},
|
||||
{
|
||||
schluessel: 'rauchmelder',
|
||||
titel: 'Rauchmelder prüfen',
|
||||
beschreibung:
|
||||
'In Bayern sind Rauchwarnmelder in Schlaf- und Kinderzimmern sowie in ' +
|
||||
'Fluren Pflicht (Art. 46 Abs. 4 BayBO), die jährliche Prüfung ebenso. ' +
|
||||
'Der Eigentümer bleibt dafür verantwortlich, auch bei Leerstand. Batterien ' +
|
||||
'wechseln, Funktionsprüfung mit Datum notieren.',
|
||||
faelligAm: abHeute(plusTage(start, 30)),
|
||||
wiederholungRrule: 'FREQ=YEARLY;INTERVAL=1',
|
||||
reihenfolge: 90,
|
||||
dringend: false,
|
||||
},
|
||||
{
|
||||
schluessel: 'heizungswartung',
|
||||
titel: 'Heizungswartung und Schornsteinfegertermin',
|
||||
beschreibung:
|
||||
'Läuft die Heizung im Frostschutzbetrieb weiter, muss sie gewartet bleiben, ' +
|
||||
'sonst fällt sie im ungünstigsten Moment aus. Der Schornsteinfeger kommt ' +
|
||||
'ohnehin und braucht Zugang — Termin und Schlüsselübergabe organisieren, ' +
|
||||
'sonst kommt eine kostenpflichtige zweite Anfahrt.',
|
||||
faelligAm: abHeute(plusTage(start, 30)),
|
||||
wiederholungRrule: 'FREQ=YEARLY;INTERVAL=1',
|
||||
reihenfolge: 100,
|
||||
dringend: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Punkte, die naechstes Jahr wieder anstehen - fuer den Hinweis in
|
||||
* der Oberflaeche, dass die Liste kein einmaliges Abhaken ist.
|
||||
*/
|
||||
export function wiederkehrendePunkte(punkte: LeerstandPunkt[]): LeerstandPunkt[] {
|
||||
return punkte.filter((p) => Boolean(p.wiederholungRrule))
|
||||
}
|
||||
|
||||
/**
|
||||
* Der naechste Termin eines wiederkehrenden Punktes nach dem Erledigen.
|
||||
* Bewusst schlicht gehalten: Woche, Quartal, Jahr - mehr kommt in dieser
|
||||
* Liste nicht vor.
|
||||
*/
|
||||
export function naechsteFaelligkeit(rrule: string, erledigtAm: Date): Date | null {
|
||||
const regel = rrule.toUpperCase()
|
||||
if (regel.includes('FREQ=WEEKLY')) return plusTage(erledigtAm, 7)
|
||||
if (regel.includes('FREQ=MONTHLY')) {
|
||||
const treffer = /INTERVAL=(\d+)/.exec(regel)
|
||||
const monate = treffer?.[1] ? Number(treffer[1]) : 1
|
||||
return plusTage(erledigtAm, monate * 30)
|
||||
}
|
||||
if (regel.includes('FREQ=YEARLY')) {
|
||||
const monat = /BYMONTH=(\d+)/.exec(regel)?.[1]
|
||||
const tagImMonat = /BYMONTHDAY=(\d+)/.exec(regel)?.[1]
|
||||
if (monat && tagImMonat) {
|
||||
return naechsterStichtagImJahr(
|
||||
plusTage(erledigtAm, 1),
|
||||
Number(monat),
|
||||
Number(tagImMonat),
|
||||
)
|
||||
}
|
||||
return plusJahre(erledigtAm, 1)
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user