Files
FPT/prisma/schema.prisma
T
admGitAICDS b42f9b385f
Deploy App / deploy (push) Successful in 1m59s
Modul-Review 3: Struktur, Dashboard, CSV-Export und Grafiken
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>
2026-07-25 12:16:07 +02:00

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])
}