Modul-Review 4: Matrix -- Kapitalverwendung, Phasendauer, Bedienung
Deploy App / deploy (push) Successful in 1m10s
Deploy App / deploy (push) Successful in 1m10s
Kapitalverwendung (Punkt C) am richtigen Ort: - Prozent-Aufteilung des bezogenen Alterskapitals wandert vom Cash-Uebergang zum Bezugs-Entscheid der PK (nur bei Kapitalbezug) bzw. der Saeule 3a -- mit zwei Guthaben liess sie sich vorher gar nicht getrennt beantworten - Dialoge fuehren neu brutto -> Steuersatz -> netto -> Verteilung - BUGFIX: Der zugeteilte Betrag erhoehte still den internen Bestand, deshalb zeigten Ziel-Element und "Kapital verteilen" eine 0. Er laeuft jetzt ueber Carry.capitalIn als Zusatzeinlage der Folgephase und ist ueberall sichtbar - Saeule 3a ist am Pensions-Uebergang neu ein offener Entscheid Phasendauer (gemeldeter Fehler): - Die Folgephase gleicht eine geaenderte Dauer aus; Gesamtdauer bleibt gleich - Vorher kappte das Tool nur die bearbeitete Phase -> Phase 2 ueberspannte danach die Pensionierung und die Invariante aus Punkt 44 kippte - Rueckfrage vorher, Blockade wenn die Folgephase unter 1 Jahr fiele - neue reine Funktion planDurationChange Bedienung: - Element-Zeile und Phasenkopf: Stift (umbenennen, beim Element inkl. Zuordnung), Papierkorb, Expand -- alle immer sichtbar - PATCH /api/elements/<id> nimmt neu auch ownerRole - Hilfetexte via Portal (wurden in scrollenden Dialogen abgeschnitten) - Verteil-Dialoge: Zuordnung je Zeile, nach vom/ins Cash gruppiert, Vorbelegung mit dem EFFEKTIVEN Wert inkl. Vererbung (zeigte vorher 0) - Matrix: gleiche Spaltenbreiten + horizontales Scrollen, "Alle auf-/ zuklappen", Kategorie-Summe in der zugeklappten Zeile - Phasen-Detailansicht nutzt die neue Aufteilungs-Grafik - Uebersicht: "Leer starten" auch im leeren Zustand Nebenbei: dritte verstuemmelte Hex-Farbe (#7c3aed) repariert, Phasen-Panel nutzt den eigenen Bestaetigungs-Dialog statt window.confirm; mehrere veraltete Referenzen und die buildCarryData-Tabelle in der Spez nachgezogen. SPEZIFIKATION 0.33. 278 -> 288 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,14 @@ import { prisma } from "@/lib/db";
|
||||
import { getOwnedElement } from "@/lib/queries";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { touchScenario } from "@/lib/versioning-db";
|
||||
import { PERSON_ONLY_CATEGORIES } from "@/lib/elements";
|
||||
|
||||
const patchSchema = z.object({ name: z.string().min(1).max(120) });
|
||||
// Name UND Zuordnung sind änderbar. Die Zuordnung läuft über `ownerRole` (nicht über eine
|
||||
// Person-ID), deshalb überlebt sie auch ein Neuanlegen der Personen im Profil-Dialog.
|
||||
const patchSchema = z.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
ownerRole: z.enum(["PERSON_A", "PERSON_B", "HOUSEHOLD"]).optional(),
|
||||
});
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
@@ -22,7 +28,20 @@ export async function PATCH(
|
||||
const parsed = patchSchema.safeParse(body);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
|
||||
await prisma.financialElement.update({ where: { id: element.id }, data: { name: parsed.data.name } });
|
||||
// AHV, PK und 3a hängen am individuellen Pensionierungszeitpunkt und müssen deshalb einer
|
||||
// Person gehören -- «Gemeinsam» wäre dort fachlich sinnlos.
|
||||
const nextOwner = parsed.data.ownerRole;
|
||||
if (nextOwner === "HOUSEHOLD" && PERSON_ONLY_CATEGORIES.includes(element.category)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Diese Kategorie muss einer Person zugeordnet bleiben." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.financialElement.update({
|
||||
where: { id: element.id },
|
||||
data: { name: parsed.data.name ?? undefined, ownerRole: nextOwner ?? undefined },
|
||||
});
|
||||
await touchScenario(element.scenarioId, userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getOwnedPhase, getOwnedScenario, toPlanInput } from "@/lib/queries";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { touchScenario } from "@/lib/versioning-db";
|
||||
import { maxPhaseDuration } from "@/lib/calculations";
|
||||
import { planDurationChange } from "@/lib/phaseplan";
|
||||
|
||||
const updatePhaseSchema = z.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
@@ -27,29 +28,45 @@ export async function PUT(
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
|
||||
let duration = parsed.data.durationYears;
|
||||
// Die Folgephase gleicht eine geänderte Dauer aus -- sonst verschieben sich alle späteren
|
||||
// Phasengrenzen und eine davon überspannt eine Pensionierung (siehe lib/phaseplan.ts).
|
||||
let neighbour: { id: string; durationYears: number } | null = null;
|
||||
|
||||
if (duration != null) {
|
||||
// Dauer ans nächste Pensionsereignis kappen (Jahre vor dieser Phase).
|
||||
const scenario = await getOwnedScenario(existing.scenarioId, userId);
|
||||
if (scenario) {
|
||||
const planInput = toPlanInput(scenario);
|
||||
// Dauer ans nächste Pensionsereignis kappen (Jahre vor dieser Phase).
|
||||
const yearsBefore = planInput.phases
|
||||
.filter((p) => p.sequenceNumber < existing.sequenceNumber)
|
||||
.reduce((s, p) => s + p.durationYears, 0);
|
||||
const cap = maxPhaseDuration(planInput.persons, yearsBefore);
|
||||
if (cap != null) duration = Math.min(duration, cap);
|
||||
duration = Math.max(1, duration);
|
||||
|
||||
const change = planDurationChange(planInput.phases, phaseId, duration);
|
||||
if (!change.ok) return NextResponse.json({ error: change.error }, { status: 400 });
|
||||
if (change.neighbourId && change.neighbourNewDuration != null) {
|
||||
neighbour = { id: change.neighbourId, durationYears: change.neighbourNewDuration };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const phase = await prisma.phase.update({
|
||||
where: { id: phaseId },
|
||||
data: {
|
||||
name: parsed.data.name ?? undefined,
|
||||
durationYears: duration ?? undefined,
|
||||
},
|
||||
const phase = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.phase.update({
|
||||
where: { id: phaseId },
|
||||
data: {
|
||||
name: parsed.data.name ?? undefined,
|
||||
durationYears: duration ?? undefined,
|
||||
},
|
||||
});
|
||||
if (neighbour) {
|
||||
await tx.phase.update({ where: { id: neighbour.id }, data: { durationYears: neighbour.durationYears } });
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
await touchScenario(existing.scenarioId, userId);
|
||||
return NextResponse.json({ phase: { id: phase.id } });
|
||||
return NextResponse.json({ phase: { id: phase.id }, adjustedNeighbour: neighbour });
|
||||
}
|
||||
|
||||
// Nur die letzte Phase kann gelöscht werden (Verkettung bleibt intakt).
|
||||
|
||||
Reference in New Issue
Block a user