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
+41 -60
View File
@@ -1,25 +1,18 @@
import { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/db";
import { phaseDataSchema, transitionDataSchema } from "@/lib/elements";
import type { PhaseData, TransitionData } from "@/lib/elements";
import type { HouseholdInput, PlanInput } from "@/lib/types";
export const phaseInclude = {
incomeEntries: true,
expenseEntries: true,
securities: true,
realEstates: true,
oneTimeEvents: true,
retirementInfos: true,
} satisfies Prisma.PhaseInclude;
export const planInclude = {
phases: {
include: phaseInclude,
orderBy: { sequenceNumber: "asc" },
phases: { orderBy: { sequenceNumber: "asc" } },
elements: {
orderBy: { orderIndex: "asc" },
include: { phaseValues: true, transitionValues: true },
},
} satisfies Prisma.PlanInclude;
export type PlanWithRelations = Prisma.PlanGetPayload<{ include: typeof planInclude }>;
export type PhaseWithRelations = Prisma.PhaseGetPayload<{ include: typeof phaseInclude }>;
export type HouseholdWithPersons = Prisma.HouseholdGetPayload<{ include: { persons: true } }>;
export function toHouseholdInput(household: HouseholdWithPersons): HouseholdInput {
@@ -36,63 +29,44 @@ export function toHouseholdInput(household: HouseholdWithPersons): HouseholdInpu
};
}
function parsePhaseData(raw: unknown): PhaseData {
const parsed = phaseDataSchema.safeParse(raw);
return parsed.success ? parsed.data : {};
}
function parseTransitionData(raw: unknown): TransitionData {
const parsed = transitionDataSchema.safeParse(raw);
return parsed.success ? parsed.data : {};
}
export function toPlanInput(plan: PlanWithRelations): PlanInput {
return {
id: plan.id,
name: plan.name,
parentPlanId: plan.parentPlanId,
branchFromPhaseId: plan.branchFromPhaseId,
retirementAgeA: plan.retirementAgeA,
retirementAgeB: plan.retirementAgeB,
phases: plan.phases.map((phase) => ({
id: phase.id,
sequenceNumber: phase.sequenceNumber,
name: phase.name,
durationYears: phase.durationYears,
inflationRate: phase.inflationRate,
incomeMode: phase.incomeMode,
incomingCapital: phase.incomingCapital,
incomeEntries: phase.incomeEntries.map((e) => ({
id: e.id,
personId: e.personId,
label: e.label,
amount: e.amount,
})),
expenseEntries: phase.expenseEntries.map((e) => ({
id: e.id,
label: e.label,
amount: e.amount,
})),
securities: phase.securities.map((s) => ({
id: s.id,
name: s.name,
startValue: s.startValue,
expectedReturn: s.expectedReturn,
annualContribution: s.annualContribution,
ownerTag: s.ownerTag,
saleTaxRate: s.saleTaxRate,
carriedBaseValue: s.carriedBaseValue,
})),
realEstates: phase.realEstates.map((re) => ({
id: re.id,
name: re.name,
purchasePrice: re.purchasePrice,
mortgage: re.mortgage,
amortization: re.amortization,
})),
oneTimeEvents: phase.oneTimeEvents.map((e) => ({
id: e.id,
type: e.type,
amount: e.amount,
description: e.description,
})),
retirementInfos: phase.retirementInfos.map((r) => ({
id: r.id,
personId: r.personId,
ahvAmount: r.ahvAmount,
pkPensionAmount: r.pkPensionAmount,
lumpSumAmount: r.lumpSumAmount,
lumpSumTaxRate: r.lumpSumTaxRate,
})),
})),
elements: plan.elements.map((e) => {
const phaseValues: Record<string, PhaseData> = {};
for (const pv of e.phaseValues) phaseValues[pv.phaseId] = parsePhaseData(pv.data);
const transitionValues: Record<string, TransitionData> = {};
for (const tv of e.transitionValues) transitionValues[tv.fromPhaseId] = parseTransitionData(tv.data);
return {
id: e.id,
category: e.category,
name: e.name,
ownerRole: e.ownerRole,
orderIndex: e.orderIndex,
phaseValues,
transitionValues,
};
}),
};
}
@@ -101,7 +75,7 @@ export async function getHouseholdOrNull(userId: string): Promise<HouseholdWithP
return prisma.household.findFirst({ where: { userId }, include: { persons: true } });
}
// Laedt einen Plan inkl. aller Phasen, aber nur wenn er dem Benutzer gehoert.
// Laedt einen Plan inkl. Phasen + Elemente, aber nur wenn er dem Benutzer gehoert.
export async function getOwnedPlan(planId: string, userId: string) {
return prisma.plan.findFirst({
where: { id: planId, household: { userId } },
@@ -115,3 +89,10 @@ export async function getOwnedPhase(phaseId: string, userId: string) {
where: { id: phaseId, plan: { household: { userId } } },
});
}
// Laedt ein Element (Basisdaten), aber nur wenn es dem Benutzer gehoert.
export async function getOwnedElement(elementId: string, userId: string) {
return prisma.financialElement.findFirst({
where: { id: elementId, plan: { household: { userId } } },
});
}