Neuer Knopf auf Plan-Ebene: Liste plus Wizard in zwei Schritten. Ein
Ist-Satz haengt am PLAN, nicht am Szenario -- die Zuordnung laeuft ueber
die Herkunfts-Kette sourceElementId.
computePlan nimmt neu { actuals }: Die Werte schnappen in jedem erfassten
Jahr auf die Realitaet und laufen von dort planmaessig weiter. Luecken
fallen auf die Plandaten zurueck. Ohne die Option unveraendert -- die 43
Golden Tests laufen durch.
Der Sprung ist keine Rendite: eigene Brueckenposition actualsCorrection
in Vermoegens- und Cash-Bruecke, sonst ginge die Zerlegung nicht auf.
Matrix: Umschalter Plan/Effektiv, im Ist-Modus mit farbiger Abweichung
statt acht Zahlen je Zelle. Zeitachse: Marker je Jahr, juengster farbig.
Vier Analysewerkzeuge mit einheitlicher Leiste (nominal/real als
Einfachauswahl, Plan/Effektiv). MC: Zielbetrag dreht mit, Startjahr
abgeleitet statt eingebbar.
Neue Tabelle ActualsSet (gegen echtes Postgres verifiziert), Module
actuals.ts und dataview.ts. Spezifikation 0.21, 27 Tests (181 -> 208).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
|
||||
// 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(
|
||||
_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 set = await prisma.actualsSet.findFirst({
|
||||
where: { id: setId, planId, plan: { userId } },
|
||||
});
|
||||
if (!set) return NextResponse.json({ error: "Datensatz nicht gefunden." }, { status: 404 });
|
||||
|
||||
await prisma.actualsSet.delete({ where: { id: setId } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
|
||||
// Ein Ist-Wert je Wurzel-Element. Beide Felder optional: Wer eine Zahl nicht kennt, lässt sie
|
||||
// weg -- die Lücke fällt in der Berechnung auf die Plandaten zurück.
|
||||
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 createSchema = 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),
|
||||
});
|
||||
|
||||
async function ownedPlan(planId: string, userId: string) {
|
||||
return prisma.plan.findFirst({ where: { id: planId, userId } });
|
||||
}
|
||||
|
||||
// Alle Ist-Sätze eines Plans, neueste zuerst.
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<{ planId: string }> }) {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
const { planId } = await params;
|
||||
|
||||
const plan = await ownedPlan(planId, userId);
|
||||
if (!plan) return NextResponse.json({ error: "Plan nicht gefunden." }, { status: 404 });
|
||||
|
||||
const rows = await prisma.actualsSet.findMany({
|
||||
where: { planId },
|
||||
orderBy: [{ year: "desc" }, { recordedOn: "desc" }],
|
||||
include: { createdBy: { select: { username: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
sets: rows.map((r) => ({
|
||||
id: r.id,
|
||||
recordedOn: r.recordedOn.toISOString().slice(0, 10),
|
||||
year: r.year,
|
||||
comment: r.comment,
|
||||
cash: r.cash,
|
||||
values: r.values,
|
||||
author: r.createdBy.username,
|
||||
createdAt: r.createdAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ planId: string }> }) {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
const { planId } = await params;
|
||||
|
||||
const plan = await ownedPlan(planId, userId);
|
||||
if (!plan) return NextResponse.json({ error: "Plan nicht gefunden." }, { status: 404 });
|
||||
|
||||
const parsed = createSchema.safeParse(await request.json());
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
|
||||
const { recordedOn, comment, cash, values } = parsed.data;
|
||||
// Für die Berechnung zählt nur die Jahreszahl -- der Rechenkern arbeitet in ganzen Jahren
|
||||
// ab Planbeginn. Das exakte Datum bleibt für Liste und Zeitachse erhalten.
|
||||
const year = Number(recordedOn.slice(0, 4));
|
||||
|
||||
const created = await prisma.actualsSet.create({
|
||||
data: {
|
||||
planId,
|
||||
recordedOn: new Date(`${recordedOn}T00:00:00.000Z`),
|
||||
year,
|
||||
comment: comment?.trim() || null,
|
||||
cash: typeof cash === "number" ? cash : null,
|
||||
values,
|
||||
createdById: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ set: { id: created.id, year: created.year } }, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user