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
Deploy App / deploy (push) Successful in 1m57s
This commit is contained in:
+41
-60
@@ -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 } } },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user