Szenario-Hierarchie: Plan als Behaelter, Diff-Markierung (V6)
Deploy App / deploy (push) Successful in 1m43s
Deploy App / deploy (push) Successful in 1m43s
Groesste Umstrukturierung bisher. Der PLAN ist neu ein schlanker Behaelter ohne Finanzdaten; die berechenbare Einheit ist das SZENARIO. Modell: - Jeder Plan bekommt beim Anlegen automatisch ein Basisszenario (isBase). - Das Grundprofil liegt am Szenario, nicht am Plan -- nur so sind Szenarien mit abweichendem PENSIONSALTER moeglich (Fruehpensionierung), das in Person steckt. - Neue Szenarien sind vollstaendige Kopien eines BELIEBIGEN Szenarios und haengen als Baum darunter (parentScenarioId); die Seitenleiste rueckt sie ein. - Kopierte Phasen/Elemente tragen Herkunfts-Verweise (sourcePhaseId, sourceElementId). Ueber den Namen zu matchen waere fragil gewesen. Abweichungs-Markierung (Diff gegen das Eltern-Szenario, live): - geaendert = gelb, neu = gruen + Badge, entfernt = graue Geisterzeile. - Markiert: Phasen-/Uebergangszellen, Element-Zeilen, Phasenkoepfe, Cash-Anfangswert, Cash-Uebergaenge, Grundprofil. Zaehler ueber der Matrix. - Eigene Theme-Tokens fuer Hell/Dunkel/Warm -- ein fester Gelbwert waere im Dunkelschema unbrauchbar. Charts vergleichen neu die Geschwister-Szenarien statt fremder Plaene. Datenmodell/Migration: - Neue Tabelle Plan; bisheriger Plan -> Scenario (IDs erhalten, damit alle Kind-Fremdschluessel gueltig bleiben); planId -> scenarioId in Person/Phase/ FinancialElement. Bestehende Szenarien werden per rekursivem CTE demselben Behaelter zugeordnet, auch mehrfach verschachtelte. - Migration VOR dem Deploy gegen echtes PostgreSQL verifiziert (PGlite, in-process), inkl. verschachtelter Szenarien und Cascade. Der Test ist als migrations.test.ts committet und sichert kuenftige Migrationen ab. API neu unter /api/scenarios/*. 10 neue Tests (48 -> 58). Spezifikation auf v0.8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+28
-6
@@ -11,9 +11,9 @@ export const planInclude = {
|
||||
orderBy: { orderIndex: "asc" },
|
||||
include: { phaseValues: true, transitionValues: true },
|
||||
},
|
||||
} satisfies Prisma.PlanInclude;
|
||||
} satisfies Prisma.ScenarioInclude;
|
||||
|
||||
export type PlanWithRelations = Prisma.PlanGetPayload<{ include: typeof planInclude }>;
|
||||
export type PlanWithRelations = Prisma.ScenarioGetPayload<{ include: typeof planInclude }>;
|
||||
|
||||
function parsePhaseData(raw: unknown): PhaseData {
|
||||
const parsed = phaseDataSchema.safeParse(raw);
|
||||
@@ -30,6 +30,7 @@ function parseCashTransition(raw: unknown): CashTransitionData {
|
||||
return parsed.success ? parsed.data : {};
|
||||
}
|
||||
|
||||
// Wandelt ein Szenario (DB) in die berechenbare Einheit (PlanInput) um.
|
||||
export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
||||
return {
|
||||
id: plan.id,
|
||||
@@ -50,6 +51,7 @@ export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
||||
name: phase.name,
|
||||
durationYears: phase.durationYears,
|
||||
cashTransition: parseCashTransition(phase.cashTransition),
|
||||
sourcePhaseId: phase.sourcePhaseId,
|
||||
})),
|
||||
elements: plan.elements.map((e) => {
|
||||
const phaseValues: Record<string, PhaseData> = {};
|
||||
@@ -64,29 +66,49 @@ export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
||||
orderIndex: e.orderIndex,
|
||||
phaseValues,
|
||||
transitionValues,
|
||||
sourceElementId: e.sourceElementId,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Laedt einen Plan inkl. Profil + Phasen + Elemente, aber nur wenn er dem Benutzer gehoert.
|
||||
// --- Ownership-Abfragen. Der Eigentuemer haengt neu am PLAN; Szenario/Phase/Element
|
||||
// erben ihn ueber die Kette Scenario -> Plan -> User. ---
|
||||
|
||||
// Laedt ein Szenario inkl. Profil + Phasen + Elemente, nur wenn es dem Benutzer gehoert.
|
||||
export async function getOwnedScenario(scenarioId: string, userId: string) {
|
||||
return prisma.scenario.findFirst({
|
||||
where: { id: scenarioId, plan: { userId } },
|
||||
include: planInclude,
|
||||
});
|
||||
}
|
||||
|
||||
// Wie oben, zusaetzlich mit den Kopfdaten (Plan, Basis-Flag, Elternteil).
|
||||
export async function getOwnedScenarioWithMeta(scenarioId: string, userId: string) {
|
||||
return prisma.scenario.findFirst({
|
||||
where: { id: scenarioId, plan: { userId } },
|
||||
include: { ...planInclude, plan: true },
|
||||
});
|
||||
}
|
||||
|
||||
// Laedt einen Plan (Behaelter) inkl. Szenario-Kopfdaten.
|
||||
export async function getOwnedPlan(planId: string, userId: string) {
|
||||
return prisma.plan.findFirst({
|
||||
where: { id: planId, userId },
|
||||
include: planInclude,
|
||||
include: { scenarios: { orderBy: { createdAt: "asc" } } },
|
||||
});
|
||||
}
|
||||
|
||||
// Laedt eine Phase (Basisdaten), aber nur wenn sie dem Benutzer gehoert.
|
||||
export async function getOwnedPhase(phaseId: string, userId: string) {
|
||||
return prisma.phase.findFirst({
|
||||
where: { id: phaseId, plan: { userId } },
|
||||
where: { id: phaseId, scenario: { plan: { 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: { userId } },
|
||||
where: { id: elementId, scenario: { plan: { userId } } },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user