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:
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getOwnedScenario } from "@/lib/queries";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
|
||||
const copySchema = z.object({ name: z.string().min(1).max(120) });
|
||||
|
||||
// Erstellt ein neues Szenario als vollstaendige Kopie eines bestehenden. Das Original wird
|
||||
// zum Elternteil -- damit haengt der Baum in der Seitenleiste und der Diff hat seine Basis.
|
||||
// Jede kopierte Phase/jedes kopierte Element traegt einen Herkunfts-Verweis auf sein
|
||||
// Gegenstueck im Original; darauf beruht die Abweichungs-Markierung im UI.
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ scenarioId: string }> }) {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
const { scenarioId } = await params;
|
||||
|
||||
const parsed = copySchema.safeParse(await request.json());
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungueltige Eingabe." }, { status: 400 });
|
||||
|
||||
const source = await getOwnedScenario(scenarioId, userId);
|
||||
if (!source) return NextResponse.json({ error: "Ursprungs-Szenario nicht gefunden." }, { status: 404 });
|
||||
|
||||
const newId = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.scenario.create({
|
||||
data: {
|
||||
planId: source.planId,
|
||||
name: parsed.data.name,
|
||||
isBase: false,
|
||||
parentScenarioId: source.id,
|
||||
householdType: source.householdType,
|
||||
inflationRateDefault: source.inflationRateDefault,
|
||||
initialCash: source.initialCash,
|
||||
persons: {
|
||||
create: source.persons.map((p) => ({
|
||||
role: p.role,
|
||||
name: p.name,
|
||||
age: p.age,
|
||||
retirementAge: p.retirementAge,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Phasen kopieren (alte -> neue Id merken, fuer die Werte-Zuordnung).
|
||||
const phaseIdMap = new Map<string, string>();
|
||||
for (const phase of source.phases) {
|
||||
const p = await tx.phase.create({
|
||||
data: {
|
||||
scenarioId: created.id,
|
||||
sequenceNumber: phase.sequenceNumber,
|
||||
name: phase.name,
|
||||
durationYears: phase.durationYears,
|
||||
cashTransition: phase.cashTransition ?? undefined,
|
||||
sourcePhaseId: phase.id,
|
||||
},
|
||||
});
|
||||
phaseIdMap.set(phase.id, p.id);
|
||||
}
|
||||
|
||||
// Elemente inkl. Phasen- und Uebergangswerten kopieren.
|
||||
for (const el of source.elements) {
|
||||
const newEl = await tx.financialElement.create({
|
||||
data: {
|
||||
scenarioId: created.id,
|
||||
category: el.category,
|
||||
name: el.name,
|
||||
ownerRole: el.ownerRole,
|
||||
orderIndex: el.orderIndex,
|
||||
sourceElementId: el.id,
|
||||
},
|
||||
});
|
||||
for (const pv of el.phaseValues) {
|
||||
const phaseId = phaseIdMap.get(pv.phaseId);
|
||||
if (!phaseId) continue;
|
||||
await tx.elementPhaseValue.create({
|
||||
data: { elementId: newEl.id, phaseId, data: pv.data as object },
|
||||
});
|
||||
}
|
||||
for (const tv of el.transitionValues) {
|
||||
const fromPhaseId = phaseIdMap.get(tv.fromPhaseId);
|
||||
if (!fromPhaseId) continue;
|
||||
await tx.elementTransitionValue.create({
|
||||
data: { elementId: newEl.id, fromPhaseId, data: tv.data as object },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return created.id;
|
||||
});
|
||||
|
||||
return NextResponse.json({ scenarioId: newId }, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user