Effektive Werte: Immobilien-Bugfix, Bearbeiten; Sidebar-Ebenen, Ring-Klick
Deploy App / deploy (push) Successful in 1m4s

Immobilien-Bugfix (gravierend):
- Der Ist-Wizard belegte den Immobilienwert mit dem EIGENKAPITAL vor
  (ElementYearPoint.value), waehrend Erfassung und Rechenkern den
  VERKEHRSWERT erwarten (propertyValue). Der Kern setzte die Zahl als
  Verkehrswert ein und liess die Hypothek stehen -> das Eigenkapital brach
  im Ist-Jahr um genau die Hypothek ein, meist ins Negative.
- Sichtbar als negative Gesamt-Abweichung trotz reiner Lohnerhoehung und als
  "wegbrechendes" Wohneigentum in der Vermoegensaufteilung.
- Feld ist neu als "Verkehrswert + Restschuld" beschriftet; 3 Regressionstests.

Ist-Datensaetze bearbeitbar:
- Klick auf die Zeile (oder "Bearbeiten") oeffnet den Satz erneut
- neuer Endpunkt PUT /api/plans/<id>/actuals/<setId>
- beim Bearbeiten ueberschreiben die Planwerte die erfassten Zahlen nicht

Ring-Klick in der Vermoegensaufteilung repariert:
- Recharts 3 reicht kein activePayload mehr durch (nur activeIndex) --
  der Handler feuerte nie, der Ring zeigte immer das Planende

Seitenleiste sauber dreistufig:
- Ebene 1 Plaene, Ebene 2 die vier Bereiche mit buendigen Symbolen,
  Ebene 3 nur die Szenarien (verschachtelt nach Herkunft)

Szenario-Liste zeigt neben der Version deren Kommentar.

SPEZIFIKATION 0.32 (neue Kapitel 3.9.6, 3.9.7). 275 -> 278 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 12:52:23 +02:00
parent b42f9b385f
commit 0761f6b3e2
8 changed files with 234 additions and 27 deletions
@@ -1,7 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@/lib/db";
import { getCurrentUserId } from "@/lib/session";
const valueSchema = z.object({
value: z.number().min(-1_000_000_000).max(1_000_000_000).optional(),
mortgage: z.number().min(0).max(1_000_000_000).optional(),
});
const updateSchema = z.object({
recordedOn: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Datum im Format JJJJ-MM-TT"),
comment: z.string().max(500).optional(),
cash: z.number().min(-1_000_000_000).max(1_000_000_000).nullable().optional(),
values: z.record(z.string(), valueSchema),
});
// Ändert einen bestehenden Ist-Satz. Wie beim Anlegen gilt: Ein Ist-Satz ist eine Beobachtung,
// keine Planänderung -- er erzeugt also KEINE Szenario-Version. Der ursprüngliche Erfasser
// (`createdById`) bleibt stehen; korrigiert wird der Datensatz selbst.
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ planId: string; setId: string }> }
) {
const userId = await getCurrentUserId();
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
const { planId, setId } = await params;
const existing = await prisma.actualsSet.findFirst({ where: { id: setId, planId, plan: { userId } } });
if (!existing) return NextResponse.json({ error: "Datensatz nicht gefunden." }, { status: 404 });
const parsed = updateSchema.safeParse(await request.json());
if (!parsed.success) {
const first = parsed.error.issues[0];
return NextResponse.json({ error: first?.message ?? "Ungültige Eingabe." }, { status: 400 });
}
const { recordedOn, comment, cash, values } = parsed.data;
const updated = await prisma.actualsSet.update({
where: { id: setId },
data: {
recordedOn: new Date(`${recordedOn}T00:00:00.000Z`),
// Für die Berechnung zählt nur die Jahreszahl (siehe POST).
year: Number(recordedOn.slice(0, 4)),
comment: comment?.trim() || null,
cash: typeof cash === "number" ? cash : null,
values,
},
});
return NextResponse.json({ set: { id: updated.id, year: updated.year } });
}
// Löscht einen Ist-Satz. Ein Ist-Satz ist eine Beobachtung, keine Planänderung -- deshalb
// gibt es hier weder Versionierung noch Wiederherstellung.
export async function DELETE(
@@ -24,7 +24,11 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
_count: { select: { elements: true, versions: true } },
// Höchste Version je Szenario -- die Liste zeigt die echte Nummer (z. B. «0.17»),
// nicht nur die Hauptversion.
versions: { orderBy: [{ major: "desc" }, { minor: "desc" }], take: 1, select: { major: true, minor: true } },
versions: {
orderBy: [{ major: "desc" }, { minor: "desc" }],
take: 1,
select: { major: true, minor: true, comment: true },
},
},
},
actuals: { orderBy: [{ year: "asc" }, { recordedOn: "asc" }] },
@@ -85,6 +89,9 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
parentScenarioId: s.parentScenarioId,
// Die tatsächliche aktuelle Version, nicht nur die Hauptversion. Ohne Historie noch keine.
version: top ? `${top.major}.${top.minor}` : null,
// Kommentar der aktuellen Version -- gesetzt wird er bei Hauptversionen und beim
// Wiederherstellen; Nebenversionen haben in der Regel keinen.
versionComment: top?.comment ?? null,
elementCount: s._count.elements,
phaseCount: computed.phases.length,
versionCount: s._count.versions,