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:
+12
-40
@@ -27,6 +27,12 @@ import { Timeline } from "@/components/Timeline";
|
||||
import { Sparkline } from "@/components/Sparkline";
|
||||
import { CapitalDistributionDialog, RateDistributionDialog } from "@/components/DistributionDialogs";
|
||||
import { capitalPot } from "@/lib/distribution";
|
||||
import {
|
||||
isRetirementTransition,
|
||||
openTransitionCount,
|
||||
transitionInactive as inactiveAtTransition,
|
||||
TRANSITION_CATEGORIES,
|
||||
} from "@/lib/decisions";
|
||||
import { Tour, TOUR_DONE_KEY } from "@/components/Tour";
|
||||
import { Button, EmptyState, InspectorShell, Modal, useConfirm, useToast } from "@/components/ui";
|
||||
import { ElementDetailDialog, PhaseDetailDialog } from "@/components/DetailView";
|
||||
@@ -75,15 +81,6 @@ const CATEGORY_ICON: Record<ElementCategory, React.ReactNode> = {
|
||||
|
||||
// Kategorien mit einem Übergangs-Entscheid. AHV ist dabei ein Sonderfall: nur beim
|
||||
// Pensions-Übergang ist die Beitragskarriere zu prüfen (siehe transitionInactive).
|
||||
const TRANSITION_CATEGORIES: ElementCategory[] = [
|
||||
"AHV",
|
||||
"PENSION_FUND",
|
||||
"PILLAR_3A",
|
||||
"REAL_ESTATE",
|
||||
"OTHER_ASSET",
|
||||
"OTHER_DEBT",
|
||||
];
|
||||
|
||||
const VALUE_CATEGORIES: ElementCategory[] = [
|
||||
"PENSION_FUND",
|
||||
"PILLAR_3A",
|
||||
@@ -244,12 +241,8 @@ export function PlanView({
|
||||
return computed.phases.find((p) => p.id === phaseId)?.elements.find((e) => e.elementId === elementId);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// isRetirementTransition/transitionInactive/openTransitionCount kommen aus lib/decisions --
|
||||
// der Bericht zaehlt mit derselben Regel (sonst meldet er andere Zahlen als die Matrix).
|
||||
|
||||
// Beitragskarriere des Element-Besitzers (nur für AHV relevant).
|
||||
function careerFor(element: ElementInput) {
|
||||
@@ -315,20 +308,8 @@ export function PlanView({
|
||||
// Am Übergang nichts (mehr) zu tun: verkauft/getilgt ODER PK/3a nach der Pensionierung
|
||||
// (Besitzer ist zu Beginn der Von-Phase bereits pensioniert -> bereits bezogen/verrentet)
|
||||
// ODER AHV ausserhalb des Pensions-Übergangs.
|
||||
function transitionInactive(el: ElementInput, fromPhase: PhaseComputed, toPhase?: PhaseComputed): boolean {
|
||||
const ce = computedElement(fromPhase.id, 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;
|
||||
}
|
||||
const transitionInactive = (el: ElementInput, fromPhase: PhaseComputed, toPhase?: PhaseComputed) =>
|
||||
inactiveAtTransition(computed, el, fromPhase, toPhase);
|
||||
|
||||
function cashTransitionFor(phaseId: string): CashTransitionData {
|
||||
return plan.phases.find((p) => p.id === phaseId)?.cashTransition ?? {};
|
||||
@@ -336,17 +317,8 @@ export function PlanView({
|
||||
|
||||
// Anzahl offener (noch nicht getroffener) Übergangs-Entscheide an einer Grenze.
|
||||
// Der Cash-Entscheid (einmalige Sonderein-/ausgaben) zählt mit.
|
||||
function transitionOpenCount(fromPhase: PhaseComputed, toPhase: PhaseComputed): number {
|
||||
let n = isCashTransitionAnswered(cashTransitionFor(fromPhase.id)) ? 0 : 1;
|
||||
for (const el of plan.elements) {
|
||||
if (!TRANSITION_CATEGORIES.includes(el.category)) continue;
|
||||
if (transitionInactive(el, fromPhase, toPhase)) continue;
|
||||
const td = el.transitionValues[fromPhase.id] ?? {};
|
||||
const retire = toPhase ? isRetirementTransition(el, fromPhase, toPhase) : false;
|
||||
if (!isTransitionAnswered(el.category, retire, td)) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
const transitionOpenCount = (fromPhase: PhaseComputed, toPhase: PhaseComputed) =>
|
||||
openTransitionCount(plan, computed, fromPhase, toPhase);
|
||||
|
||||
// Ist der Übergangs-Entscheid dieses Elements noch offen?
|
||||
function transitionUnanswered(el: ElementInput, fromPhase: PhaseComputed, toPhase: PhaseComputed): boolean {
|
||||
|
||||
Reference in New Issue
Block a user