d203e504c0
Deploy App / deploy (push) Successful in 1m44s
Sieben Anpassungen aus dem Feedback: - Grafiken liegen neu im eigenen Bereich "Grafiken" (Dialog, Button oben) statt unter der Matrix. Die Matrix bleibt die ruhige Hauptansicht. - Kennzahl "Geschaetzter Nachlass" entfernt -- sie war rechnerisch identisch mit dem nominalen Endvermoegen und suggerierte eine Zusatzinformation, die es nicht gab. - Monte-Carlo-Button nach oben zu den Szenario-Aktionen verschoben. - Neues Profilfeld "Planstart (Jahr)" (Scenario.startYear + Migration). Rein fuer die Darstellung -- die Berechnung rechnet unveraendert in relativen Jahren. Bestehende Szenarien werden auf das laufende Jahr gesetzt. - Zeitachse zeigt die Lebensphasen als Segmente (Breite = Dauer, Einfaerbung nach Phasentyp) inkl. Jahresspanne, plus Jahres-Beschriftung an den Enden. - Lebensphase bearbeiten neu als Popup statt Panel unter der Tabelle -- konsistent zu allen anderen Eingaben; Speichern schliesst. - Vermoegensverlauf ueber ALLE Jahre statt nur ueber die Phasengrenzen. Dafuer fuehrt computePlan das Vermoegen neu pro Jahr mit (YearPoint.wealthNominal/ wealthReal) -- dieselbe Groesse, die schon die Ruin-Erkennung berechnet. Damit werden Verlaeufe INNERHALB einer Phase sichtbar (z. B. Kapitalverzehr). Zwei Tests fuer das Jahres-Vermoegen (58 -> 60). Spezifikation auf v0.9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
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,
|
|
startYear: source.startYear,
|
|
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 });
|
|
}
|