Uebersicht der offenen Punkte statt Assistent, Bestaetigung je Phasenzelle
Deploy App / deploy (push) Successful in 1m55s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 15:02:15 +02:00
parent 6ff144d7e1
commit f22b0a3f27
22 changed files with 710 additions and 1006 deletions
+116
View File
@@ -0,0 +1,116 @@
// Was ist in diesem Plan noch offen?
//
// Ersetzt den FPT-Assistenten. Der Unterschied ist nicht die Groesse, sondern der Charakter:
// Der Assistent war ein ABLAUF ("tu dies, dann das") und funktionierte nur beim ersten
// Aufsetzen -- wer einen bestehenden Plan oeffnete, bekam Schritte angeboten, die laengst
// erledigt waren. Diese Uebersicht ist ein ZUSTAND. Sie leitet ab, was tatsaechlich fehlt,
// und traegt damit bei jedem Plan, in jeder Reihenfolge, auch beim zwanzigsten Szenario.
//
// Der Mechanismus dahinter ist derselbe, den die Uebergaenge seit 0.35 haben, jetzt auf
// PHASENWERTE ausgeweitet: Eine neue Lebensphase uebernimmt alle Werte der Vorphase, aber sie
// gelten als unbestaetigt, bis jemand hingeschaut hat. Vorbelegen ja, stillschweigend
// uebernehmen nein.
//
// WICHTIG: Bestaetigen heisst "ich habe hingeschaut", NICHT "festnageln". Der Haken steht
// neben den Werten, er kopiert sie nicht in die Phase -- sonst waere jede bestaetigte Phase
// eingefroren und die Feld-Vererbung (SPEZIFIKATION 3.12.4) waere hinfaellig.
import { openTransitionCount, type DecisionCounts } from "@/lib/decisions";
import type { PlanComputed } from "@/lib/calculations";
import type { PlanInput } from "@/lib/types";
// Wo steht der Plan insgesamt? Die ersten beiden Zustaende sind KEINE Zaehlung offener
// Punkte -- ohne Elemente und ohne Phasen gibt es naturgemaess nichts Offenes, der Plan ist
// aber trotzdem leer. Diese beiden Faelle brauchen eine Aufforderung, keine Statistik.
export type ReviewStage = "NO_ELEMENTS" | "NO_PHASES" | "OPEN" | "DONE";
export interface ReviewGroup {
// Sprungziel: Phasen-Id bzw. die Von-Phase eines Uebergangs.
id: string;
kind: "phase" | "transition";
title: string;
open: number;
// Kurze Klartexte, was genau fehlt.
reasons: string[];
}
export interface PlanReview {
stage: ReviewStage;
total: number;
groups: ReviewGroup[];
}
// Braucht dieses Element in dieser Phase eine Bestaetigung? Alles, was in der Phase AKTIV ist:
// Ein verkauftes Haus oder eine getilgte Schuld traegt keine Annahmen mehr.
export function needsConfirmation(
computed: PlanComputed,
phaseId: string,
elementId: string
): boolean {
const ph = computed.phases.find((p) => p.id === phaseId);
const ce = ph?.elements.find((e) => e.elementId === elementId);
return !!ce && ce.status === "ACTIVE";
}
// Ist der Phasenwert dieses Elements bestaetigt?
export function isCellConfirmed(plan: PlanInput, phaseId: string, elementId: string): boolean {
return plan.elements.find((e) => e.id === elementId)?.phaseValues[phaseId]?.confirmed === true;
}
// Unbestaetigte Zellen einer Phase.
export function unconfirmedCells(plan: PlanInput, computed: PlanComputed, phaseId: string): string[] {
return plan.elements
.filter((e) => needsConfirmation(computed, phaseId, e.id) && !isCellConfirmed(plan, phaseId, e.id))
.map((e) => e.id);
}
export function reviewPlan(plan: PlanInput, computed: PlanComputed): PlanReview {
if (plan.elements.length === 0) return { stage: "NO_ELEMENTS", total: 0, groups: [] };
if (computed.phases.length === 0) return { stage: "NO_PHASES", total: 0, groups: [] };
const groups: ReviewGroup[] = [];
computed.phases.forEach((ph, i) => {
const reasons: string[] = [];
let open = 0;
// 1. Annahmen je Element -- Renditen, Lohnentwicklung, Teuerung, Zins.
const cells = unconfirmedCells(plan, computed, ph.id);
if (cells.length > 0) {
open += cells.length;
reasons.push(`${cells.length} ${cells.length === 1 ? "Annahme" : "Annahmen"} nicht bestätigt`);
}
// 2. Die Spar- bzw. Verzehrquote. Zaehlt eigens: Man kann jede Zelle angeschaut haben und
// die Verteilung trotzdem nie getroffen haben -- dann bleibt alles still auf dem Cash.
const phase = plan.phases.find((p) => p.id === ph.id);
if (phase && phase.ratesConfirmed !== true) {
open += 1;
reasons.push(ph.isConsumption ? "Bezüge nicht verteilt" : "Sparquote nicht verteilt");
}
if (open > 0) groups.push({ id: ph.id, kind: "phase", title: ph.name, open, reasons });
// 3. Der Uebergang NACH dieser Phase.
const next = computed.phases[i + 1];
if (!next) return;
const counts: DecisionCounts = openTransitionCount(plan, computed, ph, next);
const tOpen = counts.open + counts.unconfirmed;
if (tOpen > 0) {
const r: string[] = [];
if (counts.open > 0) r.push(`${counts.open} ${counts.open === 1 ? "Entscheid" : "Entscheide"} offen`);
if (counts.unconfirmed > 0)
r.push(`${counts.unconfirmed} ${counts.unconfirmed === 1 ? "Vorgabe" : "Vorgaben"} ungeprüft`);
groups.push({
id: ph.id,
kind: "transition",
title: `Übergang nach ${next.name}`,
open: tOpen,
reasons: r,
});
}
});
const total = groups.reduce((n, g) => n + g.open, 0);
return { stage: total === 0 ? "DONE" : "OPEN", total, groups };
}