ce987b9978
Deploy App / deploy (push) Successful in 1m13s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
5.6 KiB
TypeScript
132 lines
5.6 KiB
TypeScript
// 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[];
|
|
}
|
|
|
|
// Kategorien, die in einer Phasenzelle ueberhaupt eine Annahme tragen. Seit 0.40 entstehen
|
|
// Bestaende in den Stammdaten und jaehrliche Raten im Verteil-Dialog; in der Zelle bleiben nur
|
|
// noch die Annahmen -- und die hat nicht jede Kategorie. Eine "Sonstige Schuld" traegt keine:
|
|
// Restschuld und Tilgung stehen anderswo. Eine Bestaetigung fuer eine leere Zelle zu
|
|
// verlangen, waere eine Frage ohne Gegenstand.
|
|
const CATEGORIES_WITH_ASSUMPTIONS = new Set([
|
|
"INCOME", // Lohnentwicklung
|
|
"EXPENSE", // Teuerung
|
|
"PENSION_FUND", // Verzinsung
|
|
"PILLAR_3A", // Rendite
|
|
"OTHER_ASSET", // Rendite
|
|
"REAL_ESTATE", // Hypothekarzins, Wertsteigerung, Zins-in-Ausgaben
|
|
"AHV", // Beitragsluecken, Beitrag als Nichterwerbstaetige(r)
|
|
]);
|
|
|
|
// Braucht dieses Element in dieser Phase eine Bestaetigung? Es muss in der Phase AKTIV sein
|
|
// (ein verkauftes Haus traegt keine Annahmen mehr) UND ueberhaupt eine Annahme tragen.
|
|
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" && CATEGORIES_WITH_ASSUMPTIONS.has(ce.category);
|
|
}
|
|
|
|
// 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 };
|
|
}
|