Neuer Unterpunkt "Berichte" je Plan: Liste plus Assistent (Titel, Notiz, nominal ODER real, Plan-/Ist-Daten, bis zu drei Szenarien, gespeicherte Analysen). Layout immer gleich, Auswahl bestimmt nur die Bausteine. Die PDF-Datei wird ALS DATEI abgelegt (BYTEA in Postgres, nicht im Container-Dateisystem): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein -- eine Neuerzeugung koennte das nach Aenderungen an Plan, Rechenkern oder Layout nicht garantieren. Kennzahlen je Szenario inkl. offener Entscheide. Deren Zaehlung liegt neu als reine Funktion in decisions.ts, die Matrix UND Bericht benutzen -- sonst nennen beide verschiedene Zahlen. Zu jeder Kennzahl ihre Grundlage als Verweis; die vollstaendigen Annahmen einmal je Szenario. Haftungsausschluss ist verpflichtend (per Test). Technik: pdfkit in der Node-Runtime statt Headless-Browser; @react-pdf/renderer bricht mit React 19. Als externes Paket deklariert, weil pdfkit Font-Metriken ueber Dateipfade laedt. Spezifikation 0.25 (3.11 neu), 9 Tests (212 -> 221). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// Offene Übergangs-Entscheide (SPEZIFIKATION 3.5.4).
|
||||
//
|
||||
// Die Zählung lag bisher lokal in `PlanView`. Sie wird jetzt auch vom Bericht gebraucht --
|
||||
// und zwei Umsetzungen derselben Regel driften garantiert auseinander. Deshalb hier als
|
||||
// reine Funktion, die beide benutzen.
|
||||
|
||||
import { isCashTransitionAnswered, isTransitionAnswered } from "@/components/ElementDetail";
|
||||
import type { ElementCategory } from "@/lib/elements";
|
||||
import type { ElementInput, PlanInput } from "@/lib/types";
|
||||
import type { PhaseComputed, PlanComputed } from "@/lib/calculations";
|
||||
|
||||
export const TRANSITION_CATEGORIES: ElementCategory[] = [
|
||||
"AHV",
|
||||
"PENSION_FUND",
|
||||
"PILLAR_3A",
|
||||
"REAL_ESTATE",
|
||||
"OTHER_ASSET",
|
||||
"OTHER_DEBT",
|
||||
];
|
||||
|
||||
// Wechselt der Besitzer dieses Elements an DIESER Grenze in die Pension? Entscheidet, ob
|
||||
// z. B. ein PK-Bezug überhaupt zur Wahl steht.
|
||||
export function isRetirementTransition(
|
||||
element: ElementInput,
|
||||
fromPhase: PhaseComputed,
|
||||
toPhase: PhaseComputed
|
||||
): boolean {
|
||||
if (!element.ownerRole || element.ownerRole === "HOUSEHOLD") return false;
|
||||
const before = fromPhase.persons.find((p) => p.role === element.ownerRole);
|
||||
const after = toPhase.persons.find((p) => p.role === element.ownerRole);
|
||||
return !!before?.working && !!after && !after.working;
|
||||
}
|
||||
|
||||
// Steht an dieser Grenze für dieses Element gar kein Entscheid an (bereits verkauft/getilgt,
|
||||
// AHV ausserhalb des Pensions-Übergangs, PK/3a einer bereits pensionierten Person)?
|
||||
export function transitionInactive(
|
||||
computed: PlanComputed,
|
||||
el: ElementInput,
|
||||
fromPhase: PhaseComputed,
|
||||
toPhase?: PhaseComputed
|
||||
): boolean {
|
||||
const ce = computed.phases.find((p) => p.id === fromPhase.id)?.elements.find((e) => e.elementId === el.id);
|
||||
if (ce && ce.status !== "ACTIVE") return true;
|
||||
if (el.category === "AHV") {
|
||||
return !(toPhase && isRetirementTransition(el, fromPhase, toPhase));
|
||||
}
|
||||
if (el.category === "PENSION_FUND" || el.category === "PILLAR_3A") {
|
||||
if (el.ownerRole && el.ownerRole !== "HOUSEHOLD") {
|
||||
const owner = fromPhase.persons.find((p) => p.role === el.ownerRole);
|
||||
if (owner && !owner.working) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Offene Entscheide an EINER Phasengrenze. Der Cash-Entscheid zählt mit.
|
||||
export function openTransitionCount(
|
||||
plan: PlanInput,
|
||||
computed: PlanComputed,
|
||||
fromPhase: PhaseComputed,
|
||||
toPhase: PhaseComputed
|
||||
): number {
|
||||
const cash = plan.phases.find((p) => p.id === fromPhase.id)?.cashTransition ?? {};
|
||||
let n = isCashTransitionAnswered(cash) ? 0 : 1;
|
||||
for (const el of plan.elements) {
|
||||
if (!TRANSITION_CATEGORIES.includes(el.category)) continue;
|
||||
if (transitionInactive(computed, el, fromPhase, toPhase)) continue;
|
||||
const td = el.transitionValues[fromPhase.id] ?? {};
|
||||
if (!isTransitionAnswered(el.category, isRetirementTransition(el, fromPhase, toPhase), td)) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Summe über alle Phasengrenzen -- die aktionierbarste Kennzahl des ganzen Werkzeugs.
|
||||
export function totalOpenDecisions(plan: PlanInput, computed: PlanComputed): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < computed.phases.length - 1; i++) {
|
||||
n += openTransitionCount(plan, computed, computed.phases[i], computed.phases[i + 1]);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
Reference in New Issue
Block a user