b42f9b385f
Deploy App / deploy (push) Successful in 1m59s
Versionierung: - startet neu bei 0.1; erst eine gesetzte Hauptversion macht daraus 1.0 - Szenario-Liste zeigt die echte Version (z.B. 0.17) statt "1.x", plus neue Spalte "Phasen" - Migration 20260724120000_version_zero_start (nur der Default) Plan-Dashboard: - Kacheln anklickbar (fuehren in ihren Bereich), neue Kachel "Berichte" - Plan umbenennen ueber Stift-Symbol - Ist-Abweichung nennt das Jahr des juengsten Ist-Datensatzes und ist bei positiver Abweichung gruen statt rot Szenario-Liste: - Klick auf die Zeile oeffnet die Matrix (Matrix-Knopf entfaellt) - je Zeile Kopie (Vorlage frei waehlbar) und Loeschen - laedt nach einer Loeschung neu (zeigte vorher den alten Stand) Seitenleiste: - Szenarien wieder verschachtelt nach Herkunft - Effektive Werte / Analysen / Berichte buendig zum Knoten "Szenarien" CSV-Export (neues Modul lib/csv.ts): - vier Bloecke: Kopf, Lebensphasen, ganze Matrix (Elemente x Phasen inkl. Uebergangs-Entscheide im Klartext), Jahreswerte - mit BOM (Excel-Umlaute), Dateiname transliteriert Umlaute - vorher enthielt die Datei kein einziges finanzielles Element Grafiken: - Szenario-Waehler gilt fuer alle drei Grafiken - BUGFIX Szenario-Vergleich: WealthChart nutzte den Namen als Datenschluessel -> gleichnamige Szenarien ueberschrieben sich (Legende zeigte beide, Chart nur eine). Neu die ID; stille Deckelung auf 4 Serien entfaellt - eigene Legende mit freier Farbwahl je Serie + Erklaerung des Linienstils - Vermoegensaufteilung neu: gestapelte Flaeche ueber die Planjahre + Ring fuer die relative Aufteilung zu einem waehlbaren Zeitpunkt - alle Diagrammfarben aus neuen Theme-Tokens (--chart-1..6, --chart-grid) Doku-Drift bereinigt: Kapitel 2.1, 3.2.2-3.2.7 und 3.10 beschrieben noch den Stand vor V7 (Grundprofil am Szenario, parentPlanId, Scenario.startYear, window.confirm, drei Sidebar-Unterpunkte). Nebenbei: verstuemmelte Hex-Farbe --danger-soft (warm) repariert, deutsche Plural-/Umlautfehler in den Uebersichts-Kacheln. SPEZIFIKATION 0.31. 267 -> 275 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
331 lines
11 KiB
Plaintext
331 lines
11 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[]
|
|
versions ScenarioVersion[]
|
|
actuals ActualsSet[]
|
|
analyses SavedAnalysis[]
|
|
reports Report[]
|
|
}
|
|
|
|
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.
|
|
// Szenario-EIGENE Angabe zu einer Person. Seit V7 nur noch das Pensionsalter -- es ist der
|
|
// Kern jedes Frueh-/Spaetpensionierungs-Szenarios. Name und Alter beschreiben den Haushalt
|
|
// und liegen deshalb am Plan (PlanPerson).
|
|
model Person {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
role PersonRole
|
|
retirementAge Int
|
|
|
|
@@unique([scenarioId, role])
|
|
}
|
|
|
|
// Eine Person des HAUSHALTS. Gilt fuer alle Szenarien des Plans.
|
|
model PlanPerson {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
role PersonRole
|
|
name String?
|
|
age Int
|
|
|
|
@@unique([planId, 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
|
|
|
|
// Seit V7 am Plan: Diese Angaben beschreiben den HAUSHALT, nicht eine Planungsvariante.
|
|
// Unterscheiden sie sich, ist es ein anderer Plan -- kein anderes Szenario.
|
|
householdType HouseholdType
|
|
// 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
|
|
|
|
scenarios Scenario[]
|
|
actuals ActualsSet[]
|
|
persons PlanPerson[]
|
|
analyses SavedAnalysis[]
|
|
reports Report[]
|
|
}
|
|
|
|
// Ein erzeugter PDF-Bericht (Roadmap Nr. 11). Die FERTIGE DATEI wird abgelegt, nicht nur
|
|
// ihre Definition: Ein Bericht, den man heute verschickt, muss in drei Jahren unveraendert
|
|
// wieder herunterladbar sein -- eine Neuerzeugung koennte das nach Aenderungen an Plan,
|
|
// Rechenkern oder Layout nicht garantieren.
|
|
model Report {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
|
|
title String
|
|
|
|
// Gewaehlte Parameter und das eingefrorene Berichtsmodell (alle Zahlen).
|
|
config Json @default("{}")
|
|
model Json @default("{}")
|
|
|
|
pdf Bytes
|
|
pdfBytes Int @default(0)
|
|
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([planId, createdAt])
|
|
}
|
|
|
|
// Eine festgehaltene Analyse (Roadmap Nr. 5 / Analysen-Ansicht). Haelt EINGABEN und ERGEBNIS
|
|
// als Zahlen fest -- read-only, es wird nichts neu gerechnet. Zahlen statt Bild, damit der
|
|
// spaetere PDF-Bericht daraus vektoriell zeichnen kann.
|
|
model SavedAnalysis {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
|
|
name String
|
|
type String // CHART | MONTE_CARLO | SENSITIVITY
|
|
|
|
// Denormalisierte Kerndaten fuer die Liste.
|
|
scenarioName String?
|
|
versionLabel String?
|
|
metric String @default("nominal")
|
|
source String @default("PLAN")
|
|
summary String?
|
|
|
|
inputs Json @default("{}")
|
|
result Json @default("{}")
|
|
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([planId, createdAt])
|
|
}
|
|
|
|
// Ein erfasster Stand der WIRKLICHKEIT zu einem Stichtag (Roadmap Nr. 5).
|
|
//
|
|
// Haengt am PLAN, nicht am Szenario: Das tatsaechliche PK-Guthaben am 18.8.2026 ist eine
|
|
// Zahl, unabhaengig davon, gegen welches Szenario man sie haelt. Die Zuordnung auf die
|
|
// szenario-eigenen Element-IDs erfolgt ueber die Herkunfts-Kette (lib/actuals.ts).
|
|
model ActualsSet {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
|
|
// Exaktes Datum fuer Liste und Zeitachse; fuer die Rechnung zaehlt nur `year`.
|
|
recordedOn DateTime @db.Date
|
|
year Int
|
|
comment String?
|
|
|
|
// Effektiver Cash-Bestand (Cash ist kein FinancialElement).
|
|
cash Float?
|
|
// Werte je Wurzel-Element: Record<rootElementId, { value?, mortgage? }>
|
|
values Json @default("{}")
|
|
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([planId, year])
|
|
}
|
|
|
|
// 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")
|
|
|
|
// Szenario-eigene Annahmen. Haushaltsform, Personen und Startjahr liegen seit V7 am Plan.
|
|
inflationRateDefault Float
|
|
initialCash Float @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Laufende Hauptversion. Die Nebenversionen zaehlen in ScenarioVersion.
|
|
currentMajor Int @default(0)
|
|
|
|
persons Person[]
|
|
phases Phase[]
|
|
elements FinancialElement[]
|
|
versions ScenarioVersion[]
|
|
}
|
|
|
|
// Ein festgehaltener Stand eines Szenarios (Version A.B). Haelt den VOLLSTAENDIGEN Zustand
|
|
// als JSON in der Form PlanInput -- nicht eine Differenz. Dadurch laesst sich jeder alte
|
|
// Stand ohne Umbau rechnen (computePlan), anzeigen und wiederherstellen.
|
|
model ScenarioVersion {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
|
|
major Int
|
|
minor Int
|
|
// Pflicht bei Hauptversionen; Wiederherstellungen tragen hier ihre Herkunft.
|
|
comment String?
|
|
isMajor Boolean @default(false)
|
|
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
|
|
|
snapshot Json
|
|
|
|
createdAt DateTime @default(now())
|
|
// Wird bei Zusammenfassung innerhalb einer Bearbeitungssitzung mitgezogen.
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([scenarioId, major, minor])
|
|
@@index([scenarioId, createdAt])
|
|
}
|
|
|
|
// 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])
|
|
}
|