// 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 }; }