d203e504c0
Deploy App / deploy (push) Successful in 1m44s
Sieben Anpassungen aus dem Feedback: - Grafiken liegen neu im eigenen Bereich "Grafiken" (Dialog, Button oben) statt unter der Matrix. Die Matrix bleibt die ruhige Hauptansicht. - Kennzahl "Geschaetzter Nachlass" entfernt -- sie war rechnerisch identisch mit dem nominalen Endvermoegen und suggerierte eine Zusatzinformation, die es nicht gab. - Monte-Carlo-Button nach oben zu den Szenario-Aktionen verschoben. - Neues Profilfeld "Planstart (Jahr)" (Scenario.startYear + Migration). Rein fuer die Darstellung -- die Berechnung rechnet unveraendert in relativen Jahren. Bestehende Szenarien werden auf das laufende Jahr gesetzt. - Zeitachse zeigt die Lebensphasen als Segmente (Breite = Dauer, Einfaerbung nach Phasentyp) inkl. Jahresspanne, plus Jahres-Beschriftung an den Enden. - Lebensphase bearbeiten neu als Popup statt Panel unter der Tabelle -- konsistent zu allen anderen Eingaben; Speichern schliesst. - Vermoegensverlauf ueber ALLE Jahre statt nur ueber die Phasengrenzen. Dafuer fuehrt computePlan das Vermoegen neu pro Jahr mit (YearPoint.wealthNominal/ wealthReal) -- dieselbe Groesse, die schon die Ruin-Erkennung berechnet. Damit werden Verlaeufe INNERHALB einer Phase sichtbar (z. B. Kapitalverzehr). Zwei Tests fuer das Jahres-Vermoegen (58 -> 60). Spezifikation auf v0.9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
6.4 KiB
Plaintext
191 lines
6.4 KiB
Plaintext
// FPT (Financial Planning Tool) — Datenmodell.
|
|
//
|
|
// Kernkonzept: finanzielle Elemente leben auf SZENARIO-Ebene und sind ueber alle
|
|
// Lebensphasen hinweg dieselbe Entitaet. Pro Element existiert je Lebensphase ein
|
|
// Werte-Datensatz (ElementPhaseValue) und je Uebergang ein Entscheid-Datensatz
|
|
// (ElementTransitionValue). Die kategorie-/kontextspezifischen Felder liegen als JSON,
|
|
// validiert und typisiert in der Applikationsschicht (lib/elements.ts).
|
|
//
|
|
// Rework 07/2026 (V6, Szenario-Hierarchie): Der PLAN ist neu ein schlanker Behaelter und
|
|
// traegt selbst keine Finanzdaten. Die berechenbare Einheit ist das SZENARIO -- es traegt
|
|
// das Grundprofil (Haushaltsform, Personen, Inflation, Cash-Anfangswert), die Phasenkette
|
|
// und die Elemente. Jeder Plan hat genau ein Basisszenario (isBase) und beliebig viele
|
|
// weitere Szenarien, die als Baum (parentScenarioId) darunter haengen. Kopierte Phasen und
|
|
// Elemente tragen einen Herkunfts-Verweis (sourcePhaseId / sourceElementId) auf ihr
|
|
// Gegenstueck im Eltern-Szenario -- darauf beruht die Abweichungs-Markierung im UI.
|
|
//
|
|
// Namens-Hinweis: In der Berechnungsschicht heisst die berechenbare Einheit weiterhin
|
|
// `PlanInput` / `computePlan` (sie beschreibt eine Finanzplanung -- fachlich ein Szenario).
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
passwordHash String
|
|
createdAt DateTime @default(now())
|
|
|
|
plans Plan[]
|
|
}
|
|
|
|
enum HouseholdType {
|
|
SINGLE
|
|
COUPLE
|
|
}
|
|
|
|
enum PersonRole {
|
|
PERSON_A
|
|
PERSON_B
|
|
}
|
|
|
|
enum OwnerRole {
|
|
PERSON_A
|
|
PERSON_B
|
|
HOUSEHOLD
|
|
}
|
|
|
|
enum ElementCategory {
|
|
INCOME
|
|
EXPENSE
|
|
AHV
|
|
PENSION_FUND
|
|
PILLAR_3A
|
|
REAL_ESTATE
|
|
OTHER_ASSET
|
|
OTHER_DEBT
|
|
}
|
|
|
|
// Einzelperson eines Szenarios. retirementAge ist das (szenario-eigene) Pensionsalter --
|
|
// dadurch sind Frueh-/Spaetpensionierungs-Szenarien moeglich.
|
|
model Person {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
role PersonRole
|
|
name String?
|
|
age Int
|
|
retirementAge Int
|
|
|
|
@@unique([scenarioId, role])
|
|
}
|
|
|
|
// Behaelter einer Planung. Traegt selbst KEINE Finanzdaten, nur den Namen und die Szenarien.
|
|
model Plan {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
name String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
scenarios Scenario[]
|
|
}
|
|
|
|
// Die berechenbare Einheit: Grundprofil + Phasenkette + Elemente. Genau ein Szenario je
|
|
// Plan ist das Basisszenario (isBase); weitere haengen als Baum darunter (parentScenarioId).
|
|
model Scenario {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
name String
|
|
isBase Boolean @default(false)
|
|
|
|
// Szenario, aus dem dieses kopiert wurde. Zugleich die Vergleichsbasis fuer den Diff
|
|
// (Basisszenario: null). Beim Loeschen des Elternteils bleibt der Baum flach erhalten.
|
|
parentScenarioId String?
|
|
parentScenario Scenario? @relation("ScenarioTree", fields: [parentScenarioId], references: [id], onDelete: SetNull)
|
|
children Scenario[] @relation("ScenarioTree")
|
|
|
|
householdType HouseholdType
|
|
inflationRateDefault Float
|
|
initialCash Float @default(0)
|
|
// Kalenderjahr, in dem die Planung beginnt (Jahr 1). Rein fuer die Darstellung --
|
|
// die Berechnung rechnet weiterhin in relativen Jahren ab Planbeginn.
|
|
startYear Int?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
persons Person[]
|
|
phases Phase[]
|
|
elements FinancialElement[]
|
|
}
|
|
|
|
// Ein Lebensabschnitt innerhalb eines Plans. Der Phasentyp (Erwerb/Pension/Mischung)
|
|
// wird NICHT gespeichert, sondern aus Alter + Pensionsalter abgeleitet (lib/calculations).
|
|
// Die Inflation liegt seit V5 plan-weit am Plan (inflationRateDefault), nicht mehr hier.
|
|
model Phase {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
sequenceNumber Int
|
|
name String
|
|
durationYears Int
|
|
|
|
// Gegenstueck im Eltern-Szenario (lose Referenz, kein FK) -- Grundlage des Diffs.
|
|
sourcePhaseId String?
|
|
|
|
// Entscheid fuer das Cash-Konto beim UEBERGANG NACH dieser Phase (JSON, validiert in
|
|
// lib/elements.ts): 1:1 uebernehmen oder einmaliger Zufluss/einmalige Kosten. Liegt hier
|
|
// und nicht in ElementTransitionValue, weil Cash kein FinancialElement ist.
|
|
cashTransition Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
phaseValues ElementPhaseValue[]
|
|
transitionValues ElementTransitionValue[] @relation("TransitionFromPhase")
|
|
|
|
@@unique([scenarioId, sequenceNumber])
|
|
}
|
|
|
|
// Ein finanzielles Element (szenario-weit): Kategorie + optionale Personenzuordnung.
|
|
model FinancialElement {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
category ElementCategory
|
|
name String
|
|
ownerRole OwnerRole?
|
|
orderIndex Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
// Gegenstueck im Eltern-Szenario (lose Referenz, kein FK) -- Grundlage des Diffs.
|
|
sourceElementId String?
|
|
|
|
phaseValues ElementPhaseValue[]
|
|
transitionValues ElementTransitionValue[]
|
|
}
|
|
|
|
// Werte eines Elements INNERHALB einer Lebensphase (kategorie-/kontextspezifisch, JSON).
|
|
model ElementPhaseValue {
|
|
id String @id @default(cuid())
|
|
elementId String
|
|
element FinancialElement @relation(fields: [elementId], references: [id], onDelete: Cascade)
|
|
phaseId String
|
|
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
|
data Json
|
|
|
|
@@unique([elementId, phaseId])
|
|
}
|
|
|
|
// Entscheid/Werte eines Elements beim UEBERGANG nach der Phase fromPhase (JSON).
|
|
model ElementTransitionValue {
|
|
id String @id @default(cuid())
|
|
elementId String
|
|
element FinancialElement @relation(fields: [elementId], references: [id], onDelete: Cascade)
|
|
fromPhaseId String
|
|
fromPhase Phase @relation("TransitionFromPhase", fields: [fromPhaseId], references: [id], onDelete: Cascade)
|
|
data Json
|
|
|
|
@@unique([elementId, fromPhaseId])
|
|
}
|