Rework core model: financial elements as plan-wide entities across phases; derived phase types (Erwerb/Pension/Misch) with retirement-capped durations and per-plan retirement age; AHV (gap years + couple ceiling), PK payout/annuity, 3a, real estate, other assets/debts; horizontal timeline with retirement markers; phase x element matrix with detail panel; savings/consumption quota + available-capital key figures with red status
Deploy App / deploy (push) Successful in 1m57s

This commit is contained in:
2026-07-13 07:55:58 +02:00
parent b3a3b565ac
commit b775ab77cb
27 changed files with 2407 additions and 2166 deletions
+125
View File
@@ -0,0 +1,125 @@
// Typ- und Validierungs-Layer fuer die finanziellen Elemente. Die kategorie- und
// kontextspezifischen Felder liegen in der DB als JSON; hier werden sie typisiert und
// (an der API-Grenze) mit Zod validiert.
import { z } from "zod";
export type ElementCategory =
| "INCOME"
| "EXPENSE"
| "AHV"
| "PENSION_FUND"
| "PILLAR_3A"
| "REAL_ESTATE"
| "OTHER_ASSET"
| "OTHER_DEBT";
export type OwnerRole = "PERSON_A" | "PERSON_B" | "HOUSEHOLD";
// Kategorien, deren Element zwingend genau einer Person zugeordnet ist.
export const PERSON_ONLY_CATEGORIES: ElementCategory[] = ["INCOME", "AHV", "PENSION_FUND", "PILLAR_3A"];
// Kategorien, die gemeinsam ODER pro Person erfasst werden koennen.
export const OWNER_OPTIONAL_CATEGORIES: ElementCategory[] = ["EXPENSE", "REAL_ESTATE", "OTHER_ASSET", "OTHER_DEBT"];
export const CATEGORY_LABELS: Record<ElementCategory, string> = {
INCOME: "Einkommen",
EXPENSE: "Ausgaben",
AHV: "AHV",
PENSION_FUND: "Pensionskasse",
PILLAR_3A: "Saeule 3a",
REAL_ESTATE: "Immobilie",
OTHER_ASSET: "Sonstiges Vermoegen",
OTHER_DEBT: "Sonstige Schulden",
};
// Reihenfolge der Kategorien in der Matrix (Gruppierung der Zeilen).
export const CATEGORY_ORDER: ElementCategory[] = [
"INCOME",
"EXPENSE",
"AHV",
"PENSION_FUND",
"PILLAR_3A",
"REAL_ESTATE",
"OTHER_ASSET",
"OTHER_DEBT",
];
// --- Roh-Payloads (JSON in der DB) ---
// Bewusst tolerant getippt (alle Felder optional): die Berechnung liest defensiv mit
// Defaults, das UI zeigt je nach Kontext nur die relevanten Felder.
export interface PhaseData {
// INCOME / EXPENSE
amount?: number;
// AHV
gapYears?: number;
// PENSION_FUND / PILLAR_3A / OTHER_ASSET
currentValue?: number;
startValue?: number;
expectedReturn?: number;
annualContribution?: number;
// REAL_ESTATE
purchasePrice?: number;
mortgage?: number;
amortization?: number;
// OTHER_DEBT
annualRepayment?: number;
}
export type TransitionDecision = "HOLD" | "SELL";
export type PkPayoutMode = "CAPITAL" | "PENSION" | "COMBI";
export interface TransitionData {
// PENSION_FUND / PILLAR_3A (normaler Uebergang)
withdrawal?: number;
// PENSION_FUND (Pensions-Uebergang)
payoutMode?: PkPayoutMode;
capitalAmount?: number;
conversionRate?: number;
// PENSION_FUND (Kapital) / PILLAR_3A (Pensions-Uebergang) / REAL_ESTATE
capitalTaxRate?: number;
saleTaxRate?: number;
// REAL_ESTATE / OTHER_ASSET
decision?: TransitionDecision;
salePrice?: number;
// OTHER_DEBT
immediateRepayment?: number;
}
// --- Zod-Schemas (nachsichtig: unbekannte Felder werden verworfen) ---
const nonNeg = z.number().min(0);
export const phaseDataSchema = z
.object({
amount: nonNeg.optional(),
gapYears: z.number().int().min(0).optional(),
currentValue: nonNeg.optional(),
startValue: nonNeg.optional(),
expectedReturn: z.number().min(-50).max(100).optional(),
annualContribution: nonNeg.optional(),
purchasePrice: nonNeg.optional(),
mortgage: nonNeg.optional(),
amortization: nonNeg.optional(),
annualRepayment: nonNeg.optional(),
})
.strip();
export const transitionDataSchema = z
.object({
withdrawal: nonNeg.optional(),
payoutMode: z.enum(["CAPITAL", "PENSION", "COMBI"]).optional(),
capitalAmount: nonNeg.optional(),
conversionRate: z.number().min(0).max(20).optional(),
capitalTaxRate: z.number().min(0).max(100).optional(),
saleTaxRate: z.number().min(0).max(100).optional(),
decision: z.enum(["HOLD", "SELL"]).optional(),
salePrice: nonNeg.optional(),
immediateRepayment: nonNeg.optional(),
})
.strip();
export function num(value: number | undefined | null, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}